Merge branch 'ignite-13038'
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..1958528
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,10 @@
+.git
+*Dockerfile*
+*docker-compose*
+**/build/
+**/node_modules/
+**/target/
+!web-agent/target/*.zip
+**/backend/agent_dists/*.zip
+**/backend/config/*.json
+**/backend/test
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7bf9a55
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+*.iml
+.npmrc
+build/
+node_modules/
+package-lock.json
+.idea/
diff --git a/DEVNOTES.txt b/DEVNOTES.txt
new file mode 100644
index 0000000..531c1ea
--- /dev/null
+++ b/DEVNOTES.txt
@@ -0,0 +1,145 @@
+Ignite Web Console Build Instructions
+=====================================
+1. Install MongoDB (version >=3.0) using instructions from http://docs.mongodb.org/manual/installation.
+2. Install Node.js (version >=8.0.0) using installer from https://nodejs.org/en/download/current for your OS.
+3. Change directory to 'modules/backend' and
+ run "npm install --no-optional" for download backend dependencies.
+4. Change directory to 'modules/frontend' and
+ run "npm install --no-optional" for download frontend dependencies.
+5. Build ignite-web-agent module follow instructions from 'modules/web-agent/README.txt'.
+6. Copy ignite-web-agent-<version>.zip from 'modules/web-agent/target'
+ to 'modules/backend/agent_dists' folder.
+
+Steps 1 - 4 should be executed once.
+
+Ignite Web Console Run In Development Mode
+==========================================
+1. Configure MongoDB to run as service or in terminal change dir to $MONGO_INSTALL_DIR/server/3.2/bin
+  and start MongoDB by executing "mongod".
+
+2. In new terminal change directory to 'modules/backend'.
+   If needed run "npm install --no-optional" (if dependencies changed) and run "npm start" to start backend.
+
+3. In new terminal change directory to 'modules/frontend'.
+  If needed run "npm install --no-optional" (if dependencies changed) and start webpack in development mode "npm run dev".
+
+4. In browser open: http://localhost:9000
+
+How to migrate model:
+
+1. Model will be upgraded on first start.
+2. To downgrade model execute in terminal following command: "./node_modules/.bin/migrate down <migration-name> -d <dbConnectionUri>".
+   Example: "./node_modules/.bin/migrate down add_index -d mongodb://localhost/console".
+
+
+Ignite Web Console Direct-Install Maven Build Instructions
+==========================================================
+To build direct-install archive from sources run following command in Ignite project root folder:
+"mvn clean package -pl :ignite-web-console -am -P web-console,direct-install,lgpl -DskipTests=true -DskipClientDocs -Dmaven.javadoc.skip=true"
+
+Assembled archive can be found here: `modules/target/ignite-web-console-direct-install-*.zip`.
+
+
+Ignite Web Console Docker Images Build Instructions
+===================================================
+Install Docker (version >=17.05) using instructions from https://www.docker.com/community-edition.
+
+To build docker images from sources run following command in Ignite project root folder:
+
+"mvn clean package -pl :ignite-web-console -am -P web-console,docker-image -DskipTests=true -DskipClientDocs -Dmaven.javadoc.skip=true"
+
+Prepared image can be listed with `docker images` command.
+
+
+Ignite Web Console Backend Docker Image Build Manual Instructions
+====================================================================
+Install Docker (version >=17.05) using instructions from https://www.docker.com/community-edition.
+
+1. Build Apache Ignite Web Agent archive as described in `modules/web-agent/README.txt`.
+2. Goto Web Console's root directory.
+3. Build docker image:
+
+"docker build . -t apacheignite/web-console-backend[:<version>] -f docker/compose/backend/Dockerfile"
+
+Prepared image can be listed in `docker images` command output.
+
+
+Ignite Web Console Frontend Docker Image Build Manual Instructions
+====================================================================
+Install Docker (version >=17.05) using instructions from https://www.docker.com/community-edition.
+
+1. Build Apache Ignite Web Agent archive as described in `modules/web-agent/README.txt`.
+2. Goto Web Console's root directory.
+3. Build docker image:
+
+"docker build . -t apacheignite/web-console-frontend[:<version>] -f docker/compose/frontend/Dockerfile"
+
+Prepared image can be listed in `docker images` command output.
+
+
+End-to-end tests
+================
+E2E tests are performed with TestCafe framework - https://testcafe.devexpress.com
+
+To launch tests on your local machine you will need:
+1. Install and launch MongoDB.
+2. Optionally install Chromium (https://www.chromium.org/getting-involved/download-chromium or https://chromium.woolyss.com).
+   You may use any other browser, just set 'BROWSERS' constant in 'modules/e2e/testcafe/index.js'.
+3. In new terminal change directory to 'modules/e2e/testcafe' folder and execute: "npm install".
+4. To start test environment and tests execute: "npm run test".
+
+During developing tests you may need to run some particular tests without running all suites.
+For this case you need to run environment and test separately.
+To perform it do the following:
+1. Ensure that MongoDB is up and running and all dependencies for backend and frontend are installed.
+2. Open directory "modules/e2e/testcafe" in terminal. Install dependencies for E2E testing with "npm install" command.
+3. Execute command "npm run env". This will start backend and frontend environment.
+4. Open another terminal window and run command "node index.js" in the same directory. This will run only tests without launching environment.
+
+Please refer to TestCafe documentation at https://devexpress.github.io/testcafe/documentation/test-api/test-code-structure.html#skipping-tests
+ upon how to specify which particular test should be run or skipped.
+
+You can modify the following params with environment variables:
+- DB_URL - connection string to test MongoDB. Default: mongodb://localhost/console-e2e
+- APP_URL - URL for test environment applications. Default: http://localhost:9001
+- REPORTER - Which "TestCafe" reporter to use. Set to 'teamcity' to use Teamcity reporter. Default: "spec" (native Testcafe reporter)
+
+You can run tests in docker:
+1. Install docker and docker-compose.
+2. Execute in terminal: "docker-compose up --build --abort-on-container-exit" in directory "modules/e2e".
+3. If you need to cleanup docker container then execute "docker-compose down".
+
+
+Frontend unit tests
+===================
+Unit tests are performed with Mocha framework - https://mochajs.org
+
+To launch tests on your local machine you will need:
+1. In new terminal change directory to 'modules/frontend' folder and execute: "npm install".
+2. To start test environment and tests execute: "npm run test".
+
+
+Backend unit tests
+==================
+Unit tests are performed with Mocha framework - https://mochajs.org
+
+To launch tests on your local machine you will need:
+1. In new terminal change directory to 'modules/backend' folder and execute: "npm install".
+2. To start test environment and tests execute: "npm run test".
+
+
+Web Console settings
+====================
+Web Console backend could be configured with custom parameters.
+
+See "backend/config/settings.json.sample" for list of parameters with example value.
+
+If you need custom parameters, you will need create "backend/config/settings.json" and adjust values.
+
+Web Console settings for Docker
+===============================
+Web Console backend could be configured with custom parameters.
+
+You may pass custom parameters with help of "-e" option.
+
+For example: docker run -e "server_disable_signup=true" -p 9090:80 $IMAGE_ID
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..286082c
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,36 @@
+Ignite Web Console
+======================================
+An Interactive Configuration Wizard and Management Tool for Apache Ignite
+
+The Apache Ignite Web Console includes an interactive configuration wizard which helps you create and download configuration
+ files for your Apache Ignite cluster. The tool also provides management capabilities which allow you to run SQL queries
+ on your in-memory cache as well as view execution plans, in-memory schema, and streaming charts.
+
+In order to simplify evaluation of Web Console demo mode was implemented.
+ To start demo, you need to click button "Start demo". New tab will be open with prepared demo data on each screen.
+
+ Demo for import domain model from database.
+  In this mode an in-memory H2 database will be started.
+  How to evaluate:
+    1) Go to Ignite Web Console "Domain model" screen.
+    2) Click "Import from database". You should see modal with demo description.
+    3) Click "Next" button. You should see list of available schemas.
+    4) Click "Next" button. You should see list of available tables.
+    5) Click "Next" button. You should see import options.
+    6) Select some of them and click "Save".
+
+ Demo for SQL.
+   How to evaluate:
+    In this mode internal Ignite node will be started. Cache created and populated with data.
+     1) Click "SQL" in Ignite Web Console top menu.
+     2) "Demo" notebook with preconfigured queries will be opened.
+     3) You can also execute any SQL queries for tables: "Country, Department, Employee, Parking, Car".
+
+ For example:
+  1) Enter SQL statement:
+      SELECT p.name, count(*) AS cnt FROM "ParkingCache".Parking p
+       INNER JOIN "CarCache".Car c ON (p.id) = (c.parkingId)
+       GROUP BY P.NAME
+  2) Click "Execute" button. You should get some data in table.
+  3) Click charts buttons to see auto generated charts.
+
diff --git a/assembly/README.txt b/assembly/README.txt
new file mode 100644
index 0000000..e88e345
--- /dev/null
+++ b/assembly/README.txt
@@ -0,0 +1,127 @@
+Requirements
+-------------------------------------
+1. JDK 8 suitable for your platform.
+2. Supported browsers: Chrome, Firefox, Safari, Edge.
+3. Ignite cluster should be started with `ignite-rest-http` module in classpath.
+ For this copy `ignite-rest-http` folder from `libs\optional` to `libs` folder.
+
+How to run
+-------------------------------------
+1. Unpack ignite-web-console-x.x.x.zip to some folder.
+2. Change work directory to folder where Web Console was unpacked.
+3. Start ignite-web-console-xxx executable for you platform:
+    For Linux: `sudo ./ignite-web-console-linux`
+    For macOS: `sudo ./ignite-web-console-macos`
+    For Windows: `ignite-web-console-win.exe`
+
+NOTE: For Linux and macOS `sudo` required because non-privileged users are not allowed to bind to privileged ports (port numbers below 1024).
+
+4. Open URL `localhost` in browser.
+5. Login with user `admin@admin` and password `admin`.
+6. Start web agent from folder `web agent`. For Web Agent settings see `web-agent\README.txt`.
+
+NOTE: Cluster URL should be specified in `web-agent\default.properties` in `node-uri` parameter.
+
+Technical details
+-------------------------------------
+1. Package content:
+    `libs` - this folder contains Web Console and MongoDB binaries.
+    `user_data` - this folder contains all Web Console data (registered users, created objects, ...) and
+     should be preserved in case of update to new version.
+    `web-agent` - this folder contains Web Agent.
+2. Package already contains MongoDB for macOS, Windows, RHEL, CentOs and Ubuntu on other platforms MongoDB will be downloaded on first start. MongoDB executables will be downloaded to `libs\mogodb` folder.
+3. Web console will start on default HTTP port `80` and bind to all interfaces `0.0.0.0`.
+3. To bind Web Console to specific network interface:
+    On Linux: `./ignite-web-console-linux --server:host 192.168.0.1`
+    On macOS: `sudo ./ignite-web-console-macos --server:host 192.168.0.1`
+    On Windows: `ignite-web-console-win.exe --server:host 192.168.0.1`
+4. To start Web Console on another port, for example `3000`:
+    On Linux: `sudo ./ignite-web-console-linux --server:port 3000`
+    On macOS: `./ignite-web-console-macos --server:port 3000`
+    On Windows: `ignite-web-console-win.exe --server:port 3000`
+
+All available parameters with defaults:
+    Web Console host:                              --server:host 0.0.0.0
+    Web Console port:                              --server:port 80
+
+    Enable HTTPS:                                  --server:ssl false
+    Disable self registration:                     --server:disable:signup false
+
+    MongoDB URL:                                   --mongodb:url mongodb://localhost/console
+
+    Enable account activation:                     --activation:enabled false
+    Activation timeout(milliseconds):              --activation:timeout 1800000
+    Activation send email throttle (milliseconds): --activation:sendTimeout 180000
+
+    Mail service:                                  --mail:service "gmail"
+    Signature text:                                --mail:sign "Kind regards, Apache Ignite Team"
+    Greeting text:                                 --mail:greeting "Apache Ignite Web Console"
+    Mail FROM:                                     --mail:from "Apache Ignite Web Console <someusername@somecompany.somedomain>"
+    User to send e-mail:                           --mail:auth:user "someusername@somecompany.somedomain"
+    E-mail service password:                       --mail:auth:pass ""
+
+SSL options has no default values:
+    --server:key "path to file with server.key"
+    --server:cert "path to file with server.crt"
+    --server:ca "path to file with ca.crt"
+    --server:passphrase "Password for key"
+    --server:ciphers "Comma separated ciphers list"
+    --server:secureProtocol "The TLS protocol version to use"
+    --server:clientCertEngine "Name of an OpenSSL engine which can provide the client certificate"
+    --server:pfx "Path to PFX or PKCS12 encoded private key and certificate chain"
+    --server:crl "Path to file with CRLs (Certificate Revocation Lists)"
+    --server:dhparam "Diffie Hellman parameters"
+    --server:ecdhCurve "A string describing a named curve"
+    --server:maxVersion "Optional the maximmu TLS version to allow"
+    --server:minVersion "Optional the minimum TLS version to allow"
+    --server:secureOptions "Optional OpenSSL options"
+    --server:sessionIdContext "Opaque identifier used by servers to ensure session state is not shared between applications"
+    --server:honorCipherOrder "true or false"
+    --server:requestCert "Set to true to specify whether a server should request a certificate from a connecting client"
+    --server:rejectUnauthorized "Set to true to automatically reject clients with invalid certificates"
+
+Documentation for SSL options: https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options
+
+Sample usages:
+    `ignite-web-console-win.exe --mail:auth:user "my_user@gmail.com" --mail:auth:pass "my_password"`
+    `ignite-web-console-win.exe --server:port 11443 --server:ssl true --server:requestCert true --server:key "server.key" --server:cert "server.crt" --server:ca "ca.crt" --server:passphrase "my_password"`
+
+Advanced configuration of SMTP for Web Console.
+-------------------------------------
+1. Create sub-folder "config" in folder with Web Console executable.
+2. Create in config folder file "settings.json".
+3. Specify SMTP settings in settings.json (updating to your specific names and passwords):
+
+Sample "settings.json":
+{
+    "mail": {
+        "service": "gmail",
+        "greeting": "My Company Greeting",
+        "from": "My Company Web Console <some_name@gmail.com>",
+        "sign": "Kind regards,<br>My Company Team",
+        "auth": {
+            "user": "some_name@gmail.com",
+            "pass": "my_password"
+        }
+    }
+}
+
+Web Console sends e-mails with help of NodeMailer: https://nodemailer.com.
+
+Documentation available here:
+   https://nodemailer.com/smtp
+   https://nodemailer.com/smtp/well-known
+
+In case of non GMail SMTP server it may require to change options in "settings.json" according to NodeMailer documentation.
+
+Troubleshooting
+-------------------------------------
+1. On Windows check that MongoDB is not blocked by Antivirus/Firewall/Smartscreen.
+2. Root permission is required to bind to 80 port under macOS and Linux, but you may always start Web Console
+   on another port if you don't have such permission.
+3. For extended debug output start Web Console as following:
+     On Linux execute command in terminal: `DEBUG=mongodb-* ./ignite-web-console-linux`
+     On macOS execute command in terminal: `DEBUG=mongodb-* ./ignite-web-console-macos`
+     On Windows execute two commands in terminal:
+         `SET DEBUG=mongodb-*`
+         `ignite-web-console-win.exe`
diff --git a/assembly/direct-install.xml b/assembly/direct-install.xml
new file mode 100644
index 0000000..d34cd07
--- /dev/null
+++ b/assembly/direct-install.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<assembly xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
+          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
+    <id>release-ignite-web-agent</id>
+
+    <formats>
+        <format>zip</format>
+    </formats>
+
+    <fileSets>
+        <fileSet>
+            <directory>${project.basedir}/target</directory>
+            <outputDirectory>/libs/agent_dists</outputDirectory>
+            <excludes>
+                <exclude>**/*</exclude>
+            </excludes>
+        </fileSet>
+
+        <fileSet>
+            <directory>${project.basedir}/target</directory>
+            <outputDirectory>/libs/mongodb/mongodb-download</outputDirectory>
+            <excludes>
+                <exclude>**/*</exclude>
+            </excludes>
+        </fileSet>
+
+        <fileSet>
+            <directory>${project.basedir}/target</directory>
+            <outputDirectory>/user_data</outputDirectory>
+            <excludes>
+                <exclude>**/*</exclude>
+            </excludes>
+        </fileSet>
+
+        <fileSet>
+            <directory>${basedir}/assembly</directory>
+            <outputDirectory>/</outputDirectory>
+            <includes>
+                <include>**/README*</include>
+            </includes>
+        </fileSet>
+
+        <fileSet>
+            <directory>${basedir}/backend/build</directory>
+            <outputDirectory>/</outputDirectory>
+            <includes>
+                <include>ignite-web-console-win.exe</include>
+            </includes>
+        </fileSet>
+        <fileSet>
+            <directory>${basedir}/backend/build</directory>
+            <outputDirectory>/</outputDirectory>
+            <fileMode>0755</fileMode>
+            <includes>
+                <include>ignite-web-console-*</include>
+            </includes>
+        </fileSet>
+
+        <fileSet>
+            <directory>${basedir}/web-agent/target</directory>
+            <outputDirectory>/libs/agent_dists</outputDirectory>
+            <includes>
+                <include>ignite-web-agent-${project.version}.zip</include>
+            </includes>
+        </fileSet>
+
+        <fileSet>
+            <directory>${basedir}/frontend/build</directory>
+            <outputDirectory>/libs/frontend</outputDirectory>
+        </fileSet>
+    </fileSets>
+</assembly>
diff --git a/docker/compose/backend/Dockerfile b/docker/compose/backend/Dockerfile
new file mode 100644
index 0000000..de2652c
--- /dev/null
+++ b/docker/compose/backend/Dockerfile
@@ -0,0 +1,35 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+FROM node:8-slim
+
+ENV NPM_CONFIG_LOGLEVEL error
+
+WORKDIR /opt/web-console
+
+# Install node modules for frontend and backend modules.
+COPY backend/package*.json backend/
+RUN (cd backend && npm install --no-optional --production)
+
+# Copy source.
+COPY backend backend
+
+COPY web-agent/target/ignite-web-agent-*.zip backend/agent_dists
+
+EXPOSE 3000
+
+CMD ["npm", "start"]
diff --git a/docker/compose/docker-compose.yml b/docker/compose/docker-compose.yml
new file mode 100644
index 0000000..7a2cfe6
--- /dev/null
+++ b/docker/compose/docker-compose.yml
@@ -0,0 +1,55 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+version: '3'
+
+services:
+  mongodb:
+    image: mongo:4.0
+    container_name: 'mongodb'
+    volumes:
+      # External volume for persisting data. (HOST_PATH:CONTAINER_PATH).
+      - ./data/mongo:/data/db
+
+  backend:
+    image: apacheignite/web-console-backend
+    depends_on:
+      - mongodb
+    # Restart on crash.
+    restart: always
+    environment:
+      # Port for serving frontend API
+      - server_port=3000
+      # Cookie session secret
+      - server_sessionSecret=CHANGE ME
+      # URL for mongodb connection
+      - mongodb_url=mongodb://mongodb/console
+      # Mail connection settings. Leave empty if no needed. See also settings, https://github.com/nodemailer/nodemailer
+      - mail_service=
+      - mail_sign=
+      - mail_greeting=
+      - mail_from=
+      - mail_auth_user=
+      - mail_auth_pass=
+
+  frontend:
+    image: apacheignite/web-console-frontend
+    depends_on:
+      - mongodb
+    ports:
+      # Proxy HTTP nginx port (HOST_PORT:DOCKER_PORT)
+      - 80:80
diff --git a/docker/compose/frontend/Dockerfile b/docker/compose/frontend/Dockerfile
new file mode 100644
index 0000000..07cbc46
--- /dev/null
+++ b/docker/compose/frontend/Dockerfile
@@ -0,0 +1,45 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+FROM node:8-slim as frontend-build
+
+ENV NPM_CONFIG_LOGLEVEL error
+
+WORKDIR /opt/web-console
+
+# Install node modules for frontend.
+COPY frontend/package*.json frontend/
+RUN (cd frontend && npm install --no-optional)
+
+# Copy source.
+COPY frontend frontend
+
+RUN (cd frontend && npm run build)
+
+FROM nginx:1-alpine
+
+WORKDIR /data/www
+
+COPY --from=frontend-build /opt/web-console/frontend/build .
+
+COPY docker/compose/frontend/nginx/nginx.conf /etc/nginx/nginx.conf
+COPY docker/compose/frontend/nginx/web-console.conf /etc/nginx/web-console.conf
+
+VOLUME /etc/nginx
+VOLUME /data/www
+
+EXPOSE 80
diff --git a/docker/compose/frontend/nginx/nginx.conf b/docker/compose/frontend/nginx/nginx.conf
new file mode 100644
index 0000000..c98875f
--- /dev/null
+++ b/docker/compose/frontend/nginx/nginx.conf
@@ -0,0 +1,59 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+user  nginx;
+worker_processes auto;
+
+error_log  /var/log/nginx/error.log  warn;
+pid        /var/run/nginx.pid;
+
+events {
+  use epoll;
+  worker_connections   512;
+  multi_accept         on;
+}
+
+http {
+  server_tokens        off;
+  sendfile             on;
+  aio                  on;
+  tcp_nopush           on;
+
+  keepalive_timeout    60;
+  tcp_nodelay          on;
+
+  client_max_body_size 100m;
+
+  #access log
+  log_format main '$http_host $remote_addr - $remote_user [$time_local] '
+  '"$request" $status $bytes_sent '
+  '"$http_referer" "$http_user_agent" '
+  '"$gzip_ratio"';
+
+  include /etc/nginx/mime.types;
+  default_type  application/octet-stream;
+
+  gzip              on;
+  gzip_disable      "msie6";
+  gzip_types        text/plain text/css text/xml text/javascript application/json application/x-javascript application/xml application/xml+rss application/javascript;
+  gzip_vary         on;
+  gzip_comp_level   5;
+
+  access_log  /var/log/nginx/access.log  main;
+  #conf.d
+  include web-console.conf;
+}
diff --git a/docker/compose/frontend/nginx/web-console.conf b/docker/compose/frontend/nginx/web-console.conf
new file mode 100644
index 0000000..4fbf204
--- /dev/null
+++ b/docker/compose/frontend/nginx/web-console.conf
@@ -0,0 +1,62 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+upstream backend-api {
+  server backend:3000;
+}
+
+server {
+  listen 80;
+  server_name _;
+
+  set $ignite_console_dir /data/www;
+
+  root $ignite_console_dir;
+
+  error_page 500 502 503 504 /50x.html;
+
+  location / {
+    try_files $uri /index.html = 404;
+  }
+
+  location /api/v1 {
+    proxy_set_header Host $http_host;
+    proxy_pass http://backend-api;
+  }
+
+  location /socket.io {
+    proxy_set_header Upgrade $http_upgrade;
+    proxy_set_header Connection "upgrade";
+    proxy_http_version 1.1;
+    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    proxy_set_header Host $host;
+    proxy_pass http://backend-api;
+  }
+
+  location /agents {
+    proxy_set_header Upgrade $http_upgrade;
+    proxy_set_header Connection "upgrade";
+    proxy_http_version 1.1;
+    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    proxy_set_header Host $host;
+    proxy_pass http://backend-api;
+  }
+
+  location = /50x.html {
+    root $ignite_console_dir/error_page;
+  }
+}
diff --git a/docker/web-agent/Dockerfile b/docker/web-agent/Dockerfile
new file mode 100644
index 0000000..7e45ad3
--- /dev/null
+++ b/docker/web-agent/Dockerfile
@@ -0,0 +1,46 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Start from Java 8 based on Alpine Linux image (~5Mb)
+FROM openjdk:8-jre-alpine
+
+# Provide default arguments
+ARG DEFAULT_DRIVER_FOLDER="/opt/ignite/drivers"
+ARG DEFAULT_NODE_URI="http://localhost:8080"
+ARG DEFAULT_SERVER_URI="http://localhost"
+ARG DEFAULT_TOKENS="NO_TOKENS"
+
+ENV DRIVER_FOLDER=$DEFAULT_DRIVER_FOLDER
+ENV NODE_URI=$DEFAULT_NODE_URI
+ENV SERVER_URI=$DEFAULT_SERVER_URI
+ENV TOKENS=$DEFAULT_TOKENS
+
+# Settings
+USER root
+ENV AGENT_HOME /opt/ignite/ignite-web-agent
+WORKDIR ${AGENT_HOME} 
+
+# Add missing software
+RUN apk --no-cache \
+    add bash
+
+# Copy main binary archive
+COPY ignite-web-agent* ./
+
+# Entrypoint
+CMD ./ignite-web-agent.sh -d ${DRIVER_FOLDER} -n ${NODE_URI} -s ${SERVER_URI} -t ${TOKENS}
+
diff --git a/docker/web-agent/README.txt b/docker/web-agent/README.txt
new file mode 100644
index 0000000..b217d1c
--- /dev/null
+++ b/docker/web-agent/README.txt
@@ -0,0 +1,36 @@
+Apache Ignite Web Agent Docker module
+=====================================
+Apache Ignite Web Agent Docker module provides Dockerfile and accompanying files
+for building docker image of Web Agent.
+
+
+Build image
+===========
+1) Build Apache Ignite Web Console module
+
+        mvn clean install -T 2C \
+                          -Pall-java,all-scala,licenses,web-console \
+                          -pl :ignite-web-console -am \
+                          -DskipTests
+
+2) Go to Apache Ignite Web Console Docker module directory and copy Apache
+   Ignite Web Agent's binary archive
+
+        cd docker/web-agent
+        cp -rfv ../../modules/web-agent/target/ignite-web-agent-*.zip ./
+
+3) Unpack and remove Apache Ignite Web Agent's binary archive
+
+        unzip ignite-web-agent-*.zip
+        rm -rf ignite-web-agent-*.zip
+
+4) Build docker image
+
+        docker build . -t apacheignite/web-agent[:<version>]
+
+   Prepared image will be available in local docker registry (can be seen
+   issuing `docker images` command)
+
+5) Clean up
+
+        rm -rf ignite-web-agent*
diff --git a/docker/web-console/standalone/Dockerfile b/docker/web-console/standalone/Dockerfile
new file mode 100644
index 0000000..bd03c7d
--- /dev/null
+++ b/docker/web-console/standalone/Dockerfile
@@ -0,0 +1,84 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#~~~~~~~~~~~~~~~~~~#
+#  Frontend build  #
+#~~~~~~~~~~~~~~~~~~#
+FROM node:10-stretch as frontend-build
+
+ENV NPM_CONFIG_LOGLEVEL error
+
+WORKDIR /opt/web-console
+
+# Install node modules and build sources
+COPY frontend frontend
+RUN cd frontend && \
+    npm install --no-optional && \
+    npm run build
+
+
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
+#  Web Console Standalone assemble  #
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
+FROM node:10-stretch
+
+ENV NPM_CONFIG_LOGLEVEL error
+
+# Install global node packages
+RUN npm install -g pm2
+
+# Update software sources and install missing applications
+RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 9DA31620334BD75D9DCB49F368818C72E52529D4 \
+    && echo "deb http://repo.mongodb.org/apt/debian stretch/mongodb-org/4.0 main" | tee /etc/apt/sources.list.d/mongodb-org-4.0.list
+    apt update && \
+    apt install -y --no-install-recommends \
+        nginx-light \
+        mongodb-org-server \
+        dos2unix && \
+    apt-get clean && \
+    rm -rf /var/lib/apt/lists/*
+
+WORKDIR /opt/web-console
+
+# Install node modules for backend
+COPY backend/package*.json backend/
+RUN cd backend && \
+    npm install --no-optional --production
+
+# Copy and build sources
+COPY backend backend
+RUN cd backend && \
+    npm run build
+
+# Copy Ignite Web Agent module package
+COPY ignite-web-agent-*.zip backend/agent_dists
+
+# Copy previously built frontend
+COPY --from=frontend-build /opt/web-console/frontend/build static
+
+# Copy and fix entrypoint script
+COPY docker-entrypoint.sh docker-entrypoint.sh
+RUN chmod +x docker-entrypoint.sh \
+    && dos2unix docker-entrypoint.sh
+
+# Copy nginx configuration
+COPY nginx/* /etc/nginx/
+
+EXPOSE 80
+
+ENTRYPOINT ["/opt/web-console/docker-entrypoint.sh"]
+
diff --git a/docker/web-console/standalone/README.txt b/docker/web-console/standalone/README.txt
new file mode 100644
index 0000000..c97e792
--- /dev/null
+++ b/docker/web-console/standalone/README.txt
@@ -0,0 +1,35 @@
+Apache Ignite Web Console Standalone Docker module
+==================================================
+Apache Ignite Web Console Standalone Docker module provides Dockerfile and accompanying files
+for building docker image of Web Console.
+
+
+Ignite Web Console Standalone Docker Image Build Instructions
+=============================================================
+1) Build ignite-web-console module
+
+        mvn clean install -P web-console -DskipTests -T 2C -pl :ignite-web-console -am
+
+2) Copy ignite-web-agent-<version>.zip from 'modules/web-console/web-agent/target'
+   to 'docker/web-console/standalone' directory
+
+        cp -rf modules/web-console/web-agent/target/ignite-web-agent-*.zip docker/web-console/standalone
+
+3) Go to Apache Ignite Web Console Docker module directory and copy Apache
+   Ignite Web Console's frontend and backend directory
+
+        cd docker/web-console/standalone
+        cp -rf ../../../modules/web-console/backend ./
+        cp -rf ../../../modules/web-console/frontend ./
+
+4) Build docker image
+
+        docker build . -t apacheignite/web-console-standalone:[:<version>]
+
+   Prepared image will be available in local docker registry (can be seen
+   issuing `docker images` command)
+
+5) Clean up
+
+        rm -rf backend frontend ignite-web-agent*
+
diff --git a/docker/web-console/standalone/docker-entrypoint.sh b/docker/web-console/standalone/docker-entrypoint.sh
new file mode 100644
index 0000000..6757de6
--- /dev/null
+++ b/docker/web-console/standalone/docker-entrypoint.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+/usr/bin/mongod --fork --config=/etc/mongod.conf
+
+service nginx start
+
+cd backend && pm2 start ./index.js --no-daemon
diff --git a/docker/web-console/standalone/nginx/nginx.conf b/docker/web-console/standalone/nginx/nginx.conf
new file mode 100644
index 0000000..dbc79d7
--- /dev/null
+++ b/docker/web-console/standalone/nginx/nginx.conf
@@ -0,0 +1,55 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+user  www-data;
+worker_processes  1;
+
+error_log  /var/log/nginx/error.log  warn;
+pid        /var/run/nginx.pid;
+
+events {
+    worker_connections  128;
+}
+
+http {
+    server_tokens off;
+    sendfile            on;
+    tcp_nopush          on;
+
+    keepalive_timeout   60;
+    tcp_nodelay         on;
+
+    client_max_body_size 100m;
+
+    #access log
+    log_format main '$http_host $remote_addr - $remote_user [$time_local] '
+    '"$request" $status $bytes_sent '
+    '"$http_referer" "$http_user_agent" '
+    '"$gzip_ratio"';
+
+    include /etc/nginx/mime.types;
+    default_type  application/octet-stream;
+    gzip on;
+    gzip_disable "msie6";
+    gzip_types text/plain text/css text/xml text/javascript application/json application/x-javascript application/xml application/xml+rss application/javascript;
+    gzip_vary on;
+    gzip_comp_level 5;
+
+    access_log  /var/log/nginx/access.log  main;
+    #conf.d
+    include web-console.conf ;
+}
diff --git a/docker/web-console/standalone/nginx/web-console.conf b/docker/web-console/standalone/nginx/web-console.conf
new file mode 100644
index 0000000..caf171e
--- /dev/null
+++ b/docker/web-console/standalone/nginx/web-console.conf
@@ -0,0 +1,62 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+upstream backend-api {
+  server localhost:3000;
+}
+
+server {
+  listen 80;
+  server_name _;
+
+  set $ignite_console_dir /opt/web-console/static;
+
+  root $ignite_console_dir;
+
+  error_page 500 502 503 504 /50x.html;
+
+  location / {
+    try_files $uri /index.html = 404;
+  }
+
+  location /api/v1 {
+    proxy_set_header Host $http_host;
+    proxy_pass http://backend-api;
+  }
+
+  location /socket.io {
+    proxy_set_header Upgrade $http_upgrade;
+    proxy_set_header Connection "upgrade";
+    proxy_http_version 1.1;
+    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    proxy_set_header Host $host;
+    proxy_pass http://backend-api;
+  }
+
+  location /agents {
+    proxy_set_header Upgrade $http_upgrade;
+    proxy_set_header Connection "upgrade";
+    proxy_http_version 1.1;
+    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    proxy_set_header Host $host;
+    proxy_pass http://backend-api;
+  }
+
+  location = /50x.html {
+    root $ignite_console_dir/error_page;
+  }
+}
diff --git a/licenses/apache-2.0.txt b/licenses/apache-2.0.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/licenses/apache-2.0.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/licenses/cc-by-3.0.txt b/licenses/cc-by-3.0.txt
new file mode 100644
index 0000000..bd32fa8
--- /dev/null
+++ b/licenses/cc-by-3.0.txt
@@ -0,0 +1,319 @@
+Creative Commons Legal Code
+
+Attribution 3.0 Unported
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR
+    DAMAGES RESULTING FROM ITS USE.
+
+License
+
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
+COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
+COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
+AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
+TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY
+BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS
+CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND
+CONDITIONS.
+
+1. Definitions
+
+ a. "Adaptation" means a work based upon the Work, or upon the Work and
+    other pre-existing works, such as a translation, adaptation,
+    derivative work, arrangement of music or other alterations of a
+    literary or artistic work, or phonogram or performance and includes
+    cinematographic adaptations or any other form in which the Work may be
+    recast, transformed, or adapted including in any form recognizably
+    derived from the original, except that a work that constitutes a
+    Collection will not be considered an Adaptation for the purpose of
+    this License. For the avoidance of doubt, where the Work is a musical
+    work, performance or phonogram, the synchronization of the Work in
+    timed-relation with a moving image ("synching") will be considered an
+    Adaptation for the purpose of this License.
+ b. "Collection" means a collection of literary or artistic works, such as
+    encyclopedias and anthologies, or performances, phonograms or
+    broadcasts, or other works or subject matter other than works listed
+    in Section 1(f) below, which, by reason of the selection and
+    arrangement of their contents, constitute intellectual creations, in
+    which the Work is included in its entirety in unmodified form along
+    with one or more other contributions, each constituting separate and
+    independent works in themselves, which together are assembled into a
+    collective whole. A work that constitutes a Collection will not be
+    considered an Adaptation (as defined above) for the purposes of this
+    License.
+ c. "Distribute" means to make available to the public the original and
+    copies of the Work or Adaptation, as appropriate, through sale or
+    other transfer of ownership.
+ d. "Licensor" means the individual, individuals, entity or entities that
+    offer(s) the Work under the terms of this License.
+ e. "Original Author" means, in the case of a literary or artistic work,
+    the individual, individuals, entity or entities who created the Work
+    or if no individual or entity can be identified, the publisher; and in
+    addition (i) in the case of a performance the actors, singers,
+    musicians, dancers, and other persons who act, sing, deliver, declaim,
+    play in, interpret or otherwise perform literary or artistic works or
+    expressions of folklore; (ii) in the case of a phonogram the producer
+    being the person or legal entity who first fixes the sounds of a
+    performance or other sounds; and, (iii) in the case of broadcasts, the
+    organization that transmits the broadcast.
+ f. "Work" means the literary and/or artistic work offered under the terms
+    of this License including without limitation any production in the
+    literary, scientific and artistic domain, whatever may be the mode or
+    form of its expression including digital form, such as a book,
+    pamphlet and other writing; a lecture, address, sermon or other work
+    of the same nature; a dramatic or dramatico-musical work; a
+    choreographic work or entertainment in dumb show; a musical
+    composition with or without words; a cinematographic work to which are
+    assimilated works expressed by a process analogous to cinematography;
+    a work of drawing, painting, architecture, sculpture, engraving or
+    lithography; a photographic work to which are assimilated works
+    expressed by a process analogous to photography; a work of applied
+    art; an illustration, map, plan, sketch or three-dimensional work
+    relative to geography, topography, architecture or science; a
+    performance; a broadcast; a phonogram; a compilation of data to the
+    extent it is protected as a copyrightable work; or a work performed by
+    a variety or circus performer to the extent it is not otherwise
+    considered a literary or artistic work.
+ g. "You" means an individual or entity exercising rights under this
+    License who has not previously violated the terms of this License with
+    respect to the Work, or who has received express permission from the
+    Licensor to exercise rights under this License despite a previous
+    violation.
+ h. "Publicly Perform" means to perform public recitations of the Work and
+    to communicate to the public those public recitations, by any means or
+    process, including by wire or wireless means or public digital
+    performances; to make available to the public Works in such a way that
+    members of the public may access these Works from a place and at a
+    place individually chosen by them; to perform the Work to the public
+    by any means or process and the communication to the public of the
+    performances of the Work, including by public digital performance; to
+    broadcast and rebroadcast the Work by any means including signs,
+    sounds or images.
+ i. "Reproduce" means to make copies of the Work by any means including
+    without limitation by sound or visual recordings and the right of
+    fixation and reproducing fixations of the Work, including storage of a
+    protected performance or phonogram in digital form or other electronic
+    medium.
+
+2. Fair Dealing Rights. Nothing in this License is intended to reduce,
+limit, or restrict any uses free from copyright or rights arising from
+limitations or exceptions that are provided for in connection with the
+copyright protection under copyright law or other applicable laws.
+
+3. License Grant. Subject to the terms and conditions of this License,
+Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
+perpetual (for the duration of the applicable copyright) license to
+exercise the rights in the Work as stated below:
+
+ a. to Reproduce the Work, to incorporate the Work into one or more
+    Collections, and to Reproduce the Work as incorporated in the
+    Collections;
+ b. to create and Reproduce Adaptations provided that any such Adaptation,
+    including any translation in any medium, takes reasonable steps to
+    clearly label, demarcate or otherwise identify that changes were made
+    to the original Work. For example, a translation could be marked "The
+    original work was translated from English to Spanish," or a
+    modification could indicate "The original work has been modified.";
+ c. to Distribute and Publicly Perform the Work including as incorporated
+    in Collections; and,
+ d. to Distribute and Publicly Perform Adaptations.
+ e. For the avoidance of doubt:
+
+     i. Non-waivable Compulsory License Schemes. In those jurisdictions in
+        which the right to collect royalties through any statutory or
+        compulsory licensing scheme cannot be waived, the Licensor
+        reserves the exclusive right to collect such royalties for any
+        exercise by You of the rights granted under this License;
+    ii. Waivable Compulsory License Schemes. In those jurisdictions in
+        which the right to collect royalties through any statutory or
+        compulsory licensing scheme can be waived, the Licensor waives the
+        exclusive right to collect such royalties for any exercise by You
+        of the rights granted under this License; and,
+   iii. Voluntary License Schemes. The Licensor waives the right to
+        collect royalties, whether individually or, in the event that the
+        Licensor is a member of a collecting society that administers
+        voluntary licensing schemes, via that society, from any exercise
+        by You of the rights granted under this License.
+
+The above rights may be exercised in all media and formats whether now
+known or hereafter devised. The above rights include the right to make
+such modifications as are technically necessary to exercise the rights in
+other media and formats. Subject to Section 8(f), all rights not expressly
+granted by Licensor are hereby reserved.
+
+4. Restrictions. The license granted in Section 3 above is expressly made
+subject to and limited by the following restrictions:
+
+ a. You may Distribute or Publicly Perform the Work only under the terms
+    of this License. You must include a copy of, or the Uniform Resource
+    Identifier (URI) for, this License with every copy of the Work You
+    Distribute or Publicly Perform. You may not offer or impose any terms
+    on the Work that restrict the terms of this License or the ability of
+    the recipient of the Work to exercise the rights granted to that
+    recipient under the terms of the License. You may not sublicense the
+    Work. You must keep intact all notices that refer to this License and
+    to the disclaimer of warranties with every copy of the Work You
+    Distribute or Publicly Perform. When You Distribute or Publicly
+    Perform the Work, You may not impose any effective technological
+    measures on the Work that restrict the ability of a recipient of the
+    Work from You to exercise the rights granted to that recipient under
+    the terms of the License. This Section 4(a) applies to the Work as
+    incorporated in a Collection, but this does not require the Collection
+    apart from the Work itself to be made subject to the terms of this
+    License. If You create a Collection, upon notice from any Licensor You
+    must, to the extent practicable, remove from the Collection any credit
+    as required by Section 4(b), as requested. If You create an
+    Adaptation, upon notice from any Licensor You must, to the extent
+    practicable, remove from the Adaptation any credit as required by
+    Section 4(b), as requested.
+ b. If You Distribute, or Publicly Perform the Work or any Adaptations or
+    Collections, You must, unless a request has been made pursuant to
+    Section 4(a), keep intact all copyright notices for the Work and
+    provide, reasonable to the medium or means You are utilizing: (i) the
+    name of the Original Author (or pseudonym, if applicable) if supplied,
+    and/or if the Original Author and/or Licensor designate another party
+    or parties (e.g., a sponsor institute, publishing entity, journal) for
+    attribution ("Attribution Parties") in Licensor's copyright notice,
+    terms of service or by other reasonable means, the name of such party
+    or parties; (ii) the title of the Work if supplied; (iii) to the
+    extent reasonably practicable, the URI, if any, that Licensor
+    specifies to be associated with the Work, unless such URI does not
+    refer to the copyright notice or licensing information for the Work;
+    and (iv) , consistent with Section 3(b), in the case of an Adaptation,
+    a credit identifying the use of the Work in the Adaptation (e.g.,
+    "French translation of the Work by Original Author," or "Screenplay
+    based on original Work by Original Author"). The credit required by
+    this Section 4 (b) may be implemented in any reasonable manner;
+    provided, however, that in the case of a Adaptation or Collection, at
+    a minimum such credit will appear, if a credit for all contributing
+    authors of the Adaptation or Collection appears, then as part of these
+    credits and in a manner at least as prominent as the credits for the
+    other contributing authors. For the avoidance of doubt, You may only
+    use the credit required by this Section for the purpose of attribution
+    in the manner set out above and, by exercising Your rights under this
+    License, You may not implicitly or explicitly assert or imply any
+    connection with, sponsorship or endorsement by the Original Author,
+    Licensor and/or Attribution Parties, as appropriate, of You or Your
+    use of the Work, without the separate, express prior written
+    permission of the Original Author, Licensor and/or Attribution
+    Parties.
+ c. Except as otherwise agreed in writing by the Licensor or as may be
+    otherwise permitted by applicable law, if You Reproduce, Distribute or
+    Publicly Perform the Work either by itself or as part of any
+    Adaptations or Collections, You must not distort, mutilate, modify or
+    take other derogatory action in relation to the Work which would be
+    prejudicial to the Original Author's honor or reputation. Licensor
+    agrees that in those jurisdictions (e.g. Japan), in which any exercise
+    of the right granted in Section 3(b) of this License (the right to
+    make Adaptations) would be deemed to be a distortion, mutilation,
+    modification or other derogatory action prejudicial to the Original
+    Author's honor and reputation, the Licensor will waive or not assert,
+    as appropriate, this Section, to the fullest extent permitted by the
+    applicable national law, to enable You to reasonably exercise Your
+    right under Section 3(b) of this License (right to make Adaptations)
+    but not otherwise.
+
+5. Representations, Warranties and Disclaimer
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
+OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
+KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
+INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
+FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
+LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
+WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
+OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
+
+6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE
+LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR
+ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES
+ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS
+BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. Termination
+
+ a. This License and the rights granted hereunder will terminate
+    automatically upon any breach by You of the terms of this License.
+    Individuals or entities who have received Adaptations or Collections
+    from You under this License, however, will not have their licenses
+    terminated provided such individuals or entities remain in full
+    compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
+    survive any termination of this License.
+ b. Subject to the above terms and conditions, the license granted here is
+    perpetual (for the duration of the applicable copyright in the Work).
+    Notwithstanding the above, Licensor reserves the right to release the
+    Work under different license terms or to stop distributing the Work at
+    any time; provided, however that any such election will not serve to
+    withdraw this License (or any other license that has been, or is
+    required to be, granted under the terms of this License), and this
+    License will continue in full force and effect unless terminated as
+    stated above.
+
+8. Miscellaneous
+
+ a. Each time You Distribute or Publicly Perform the Work or a Collection,
+    the Licensor offers to the recipient a license to the Work on the same
+    terms and conditions as the license granted to You under this License.
+ b. Each time You Distribute or Publicly Perform an Adaptation, Licensor
+    offers to the recipient a license to the original Work on the same
+    terms and conditions as the license granted to You under this License.
+ c. If any provision of this License is invalid or unenforceable under
+    applicable law, it shall not affect the validity or enforceability of
+    the remainder of the terms of this License, and without further action
+    by the parties to this agreement, such provision shall be reformed to
+    the minimum extent necessary to make such provision valid and
+    enforceable.
+ d. No term or provision of this License shall be deemed waived and no
+    breach consented to unless such waiver or consent shall be in writing
+    and signed by the party to be charged with such waiver or consent.
+ e. This License constitutes the entire agreement between the parties with
+    respect to the Work licensed here. There are no understandings,
+    agreements or representations with respect to the Work not specified
+    here. Licensor shall not be bound by any additional provisions that
+    may appear in any communication from You. This License may not be
+    modified without the mutual written agreement of the Licensor and You.
+ f. The rights granted under, and the subject matter referenced, in this
+    License were drafted utilizing the terminology of the Berne Convention
+    for the Protection of Literary and Artistic Works (as amended on
+    September 28, 1979), the Rome Convention of 1961, the WIPO Copyright
+    Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996
+    and the Universal Copyright Convention (as revised on July 24, 1971).
+    These rights and subject matter take effect in the relevant
+    jurisdiction in which the License terms are sought to be enforced
+    according to the corresponding provisions of the implementation of
+    those treaty provisions in the applicable national law. If the
+    standard suite of rights granted under applicable copyright law
+    includes additional rights not granted under this License, such
+    additional rights are deemed to be included in the License; this
+    License is not intended to restrict the license of any rights under
+    applicable law.
+
+
+Creative Commons Notice
+
+    Creative Commons is not a party to this License, and makes no warranty
+    whatsoever in connection with the Work. Creative Commons will not be
+    liable to You or any party on any legal theory for any damages
+    whatsoever, including without limitation any general, special,
+    incidental or consequential damages arising in connection to this
+    license. Notwithstanding the foregoing two (2) sentences, if Creative
+    Commons has expressly identified itself as the Licensor hereunder, it
+    shall have all rights and obligations of Licensor.
+
+    Except for the limited purpose of indicating to the public that the
+    Work is licensed under the CCPL, Creative Commons does not authorize
+    the use by either party of the trademark "Creative Commons" or any
+    related trademark or logo of Creative Commons without the prior
+    written consent of Creative Commons. Any permitted use will be in
+    compliance with Creative Commons' then-current trademark usage
+    guidelines, as may be published on its website or otherwise made
+    available upon request from time to time. For the avoidance of doubt,
+    this trademark restriction does not form part of this License.
+
+    Creative Commons may be contacted at https://creativecommons.org/.
\ No newline at end of file
diff --git a/modules/backend/.eslintrc b/modules/backend/.eslintrc
new file mode 100644
index 0000000..b288d07
--- /dev/null
+++ b/modules/backend/.eslintrc
@@ -0,0 +1,169 @@
+env:
+    es6: true
+    browser: false
+    mocha: true
+parserOptions:
+    sourceType: module
+    ecmaVersion: 2018
+
+rules:
+    arrow-parens: [1, "always"]
+    arrow-spacing: [1, { "before": true, "after": true }]
+    accessor-pairs: 2
+    block-scoped-var: 2
+    brace-style: [0, "1tbs"]
+    comma-dangle: [2, "never"]
+    comma-spacing: [2, {"before": false, "after": true}]
+    comma-style: [2, "last"]
+    complexity: [1, 40]
+    computed-property-spacing: [2, "never"]
+    consistent-return: 0
+    consistent-this: [0, "that"]
+    constructor-super: 2
+    curly: [2, "multi-or-nest"]
+    default-case: 2
+    dot-location: 0
+    dot-notation: [2, { "allowKeywords": true }]
+    eol-last: 2
+    eqeqeq: 2
+    func-names: 0
+    func-style: [0, "declaration"]
+    generator-star-spacing: 0
+    guard-for-in: 1
+    handle-callback-err: 0
+    id-length: [2, {"min": 1, "max": 60}]
+    indent: [2, 4, {"SwitchCase": 1, "MemberExpression": "off", "CallExpression": {"arguments": "off"}}]
+    key-spacing: [2, { "beforeColon": false, "afterColon": true }]
+    lines-around-comment: 0
+    linebreak-style: [0, "unix"]
+    max-depth: [0, 4]
+    max-len: [0, 120, 4]
+    max-nested-callbacks: [1, 4]
+    max-params: [0, 3]
+    max-statements: [0, 10]
+    new-cap: 2
+    new-parens: 2
+    no-alert: 2
+    no-array-constructor: 2
+    no-bitwise: 0
+    no-caller: 2
+    no-catch-shadow: 2
+    no-cond-assign: 2
+    no-console: 0
+    no-constant-condition: 2
+    no-continue: 0
+    no-class-assign: 2
+    no-const-assign: 2
+    no-control-regex: 2
+    no-debugger: 2
+    no-delete-var: 2
+    no-div-regex: 0
+    no-dupe-keys: 2
+    no-dupe-args: 2
+    no-duplicate-case: 2
+    no-else-return: 2
+    no-empty: 2
+    no-empty-character-class: 2
+    no-eq-null: 2
+    no-eval: 2
+    no-ex-assign: 2
+    no-extend-native: 2
+    no-extra-bind: 2
+    no-extra-boolean-cast: 2
+    no-extra-parens: 0
+    no-extra-semi: 2
+    no-fallthrough: 2
+    no-floating-decimal: 1
+    no-func-assign: 2
+    no-implied-eval: 2
+    no-inline-comments: 0
+    no-inner-declarations: [2, "functions"]
+    no-invalid-regexp: 2
+    no-irregular-whitespace: 2
+    no-iterator: 2
+    no-label-var: 2
+    no-labels: 2
+    no-lone-blocks: 2
+    no-lonely-if: 2
+    no-implicit-coercion: [2, {"boolean": false, "number": true, "string": true}]
+    no-loop-func: 2
+    no-mixed-requires: [0, false]
+    no-mixed-spaces-and-tabs: [2, true]
+    no-multi-spaces: ["error", {"exceptions": { "VariableDeclarator": true }}]
+    no-multi-str: 2
+    no-multiple-empty-lines: [0, {"max": 2}]
+    no-native-reassign: 2
+    no-negated-in-lhs: 2
+    no-nested-ternary: 0
+    no-new: 2
+    no-new-func: 2
+    no-new-object: 2
+    no-new-require: 0
+    no-new-wrappers: 2
+    no-obj-calls: 2
+    no-octal: 2
+    no-octal-escape: 2
+    no-param-reassign: 0
+    no-path-concat: 0
+    no-plusplus: 0
+    no-process-env: 0
+    no-process-exit: 0
+    no-proto: 2
+    no-redeclare: 2
+    no-regex-spaces: 1
+    no-restricted-modules: 0
+    no-script-url: 0
+    no-self-compare: 2
+    no-sequences: 2
+    no-shadow: 0
+    no-shadow-restricted-names: 2
+    no-spaced-func: 2
+    no-sparse-arrays: 1
+    no-sync: 0
+    no-ternary: 0
+    no-trailing-spaces: ["error", {"ignoreComments": true}]
+    no-throw-literal: 0
+    no-this-before-super: 2
+    no-unexpected-multiline: 2
+    // The rule produces undesired results with TS
+    // no-undef: 2
+    no-undef-init: 2
+    no-undefined: 2
+    no-unneeded-ternary: 2
+    no-unreachable: 2
+    no-unused-expressions: [2, { allowShortCircuit: true }]
+    no-unused-vars: [0, {"vars": "all", "args": "after-used"}]
+    typescript/no-unused-vars: [0]
+    no-useless-call: 2
+    no-void: 0
+    no-var: 2
+    no-warning-comments: 0
+    no-with: 2
+    newline-after-var: 0
+    object-shorthand: [2, "always"]
+    one-var: [2, "never"]
+    operator-assignment: [2, "always"]
+    operator-linebreak: 0
+    padded-blocks: 0
+    prefer-const: 1
+    prefer-spread: 2
+    quote-props: [2, "as-needed"]
+    quotes: [2, "single", {"allowTemplateLiterals": true}]
+    radix: 1
+    semi: [2, "always"]
+    semi-spacing: [2, {"before": false, "after": true}]
+    sort-vars: 0
+    keyword-spacing: 2
+    space-before-blocks: [2, "always"]
+    space-before-function-paren: [2, "never"]
+    space-in-parens: 0
+    space-infix-ops: 2
+    space-unary-ops: [2, { "words": true, "nonwords": false }]
+    spaced-comment: [1, "always", {"markers": ["/"]}]
+    use-isnan: 2
+    valid-jsdoc: 0
+    valid-typeof: 2
+    vars-on-top: 2
+    wrap-iife: 0
+    wrap-regex: 0
+    yoda: [2, "never"]
diff --git a/modules/backend/.gitignore b/modules/backend/.gitignore
new file mode 100644
index 0000000..73bd1a1
--- /dev/null
+++ b/modules/backend/.gitignore
@@ -0,0 +1,3 @@
+agent_dists/*.zip
+config/*.json
+!package-lock.json
diff --git a/modules/backend/agent_dists/README.txt b/modules/backend/agent_dists/README.txt
new file mode 100644
index 0000000..e0a67c6
--- /dev/null
+++ b/modules/backend/agent_dists/README.txt
@@ -0,0 +1,6 @@
+Ignite Web Console
+======================================
+
+This is a default directory for agent distributions.
+
+A custom directory can be set in `backend/config/settings.json`.
diff --git a/modules/backend/app/agentSocket.js b/modules/backend/app/agentSocket.js
new file mode 100644
index 0000000..0df5c5e
--- /dev/null
+++ b/modules/backend/app/agentSocket.js
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const _ = require('lodash');
+
+// Fire me up!
+
+/**
+ * Module interaction with agents.
+ */
+module.exports = {
+    implements: 'agent-socket'
+};
+
+/**
+ * @returns {AgentSocket}
+ */
+module.exports.factory = function() {
+    /**
+     * Connected agent descriptor.
+     */
+    class AgentSocket {
+        /**
+         * @param {Socket} socket Socket for interaction.
+         * @param {Object} accounts Active accounts.
+         * @param {Array.<String>} tokens Agent tokens.
+         * @param {String} demoEnabled Demo enabled.
+         */
+        constructor(socket, accounts, tokens, demoEnabled) {
+            Object.assign(this, {
+                accounts,
+                cluster: null,
+                demo: {
+                    enabled: demoEnabled,
+                    browserSockets: []
+                },
+                socket,
+                tokens
+            });
+        }
+
+        resetToken(oldToken) {
+            _.pull(this.tokens, oldToken);
+
+            this.emitEvent('agent:reset:token', oldToken)
+                .then(() => {
+                    if (_.isEmpty(this.tokens) && this.socket.connected)
+                        this.socket.close();
+                });
+        }
+
+        /**
+         * Send event to agent.
+         *
+         * @this {AgentSocket}
+         * @param {String} event Event name.
+         * @param {Array.<Object>} args - Transmitted arguments.
+         * @param {Function} [callback] on finish
+         */
+        _emit(event, args, callback) {
+            if (!this.socket.connected) {
+                if (callback)
+                    callback('org.apache.ignite.agent.AgentException: Connection is closed');
+
+                return;
+            }
+
+            this.socket.emit(event, ...args, callback);
+        }
+
+        /**
+         * Send event to agent.
+         *
+         * @param {String} event - Event name.
+         * @param {Object?} args - Transmitted arguments.
+         * @returns {Promise}
+         */
+        emitEvent(event, ...args) {
+            return new Promise((resolve, reject) =>
+                this._emit(event, args, (resErr, res) => {
+                    if (resErr)
+                        return reject(resErr);
+
+                    resolve(res);
+                })
+            );
+        }
+
+        restResultParse(res) {
+            if (res.status === 0)
+                return JSON.parse(res.data);
+
+            if (res.status === 2)
+                throw new Error('AgentSocket failed to authenticate in grid. Please check agent\'s login and password or node port.');
+
+            throw new Error(res.error);
+        }
+
+        /**
+         * @param {Socket} browserSocket
+         */
+        attachToDemoCluster(browserSocket) {
+            this.demo.browserSockets.push(...browserSocket);
+        }
+    }
+
+    return AgentSocket;
+};
diff --git a/modules/backend/app/agentsHandler.js b/modules/backend/app/agentsHandler.js
new file mode 100644
index 0000000..f6e3da9
--- /dev/null
+++ b/modules/backend/app/agentsHandler.js
@@ -0,0 +1,419 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const uuid = require('uuid/v4');
+
+const fs = require('fs');
+const path = require('path');
+const JSZip = require('jszip');
+const socketio = require('socket.io');
+const _ = require('lodash');
+
+// Fire me up!
+
+/**
+ * Module interaction with agents.
+ */
+module.exports = {
+    implements: 'agents-handler',
+    inject: ['settings', 'mongo', 'agent-socket']
+};
+
+/**
+ * @param settings
+ * @param mongo
+ * @param {AgentSocket} AgentSocket
+ * @returns {AgentsHandler}
+ */
+module.exports.factory = function(settings, mongo, AgentSocket) {
+    class AgentSockets {
+        constructor() {
+            /**
+             * @type {Map.<String, Array.<String>>}
+             */
+            this.sockets = new Map();
+        }
+
+        get(account) {
+            let sockets = this.sockets.get(account._id.toString());
+
+            if (_.isEmpty(sockets))
+                this.sockets.set(account._id.toString(), sockets = []);
+
+            return sockets;
+        }
+
+        /**
+         * @param {AgentSocket} sock
+         * @param {String} account
+         * @return {Array.<AgentSocket>}
+         */
+        add(account, sock) {
+            const sockets = this.get(account);
+
+            sockets.push(sock);
+        }
+
+        /**
+         * @param {Socket} browserSocket
+         * @return {AgentSocket}
+         */
+        find(browserSocket) {
+            const {_id} = browserSocket.request.user;
+
+            const sockets = this.sockets.get(_id);
+
+            return _.find(sockets, (sock) => _.includes(sock.demo.browserSockets, browserSocket));
+        }
+    }
+
+    class Cluster {
+        constructor(top) {
+            const clusterName = top.clusterName;
+
+            this.id = _.isEmpty(top.clusterId) ? uuid() : top.clusterId;
+            this.name = _.isEmpty(clusterName) ? `Cluster ${this.id.substring(0, 8).toUpperCase()}` : clusterName;
+            this.nids = top.nids;
+            this.addresses = top.addresses;
+            this.clients = top.clients;
+            this.clusterVersion = top.clusterVersion;
+            this.active = top.active;
+            this.secured = top.secured;
+        }
+
+        isSameCluster(top) {
+            return _.intersection(this.nids, top.nids).length > 0;
+        }
+
+        update(top) {
+            this.clusterVersion = top.clusterVersion;
+            this.nids = top.nids;
+            this.addresses = top.addresses;
+            this.clients = top.clients;
+            this.clusterVersion = top.clusterVersion;
+            this.active = top.active;
+            this.secured = top.secured;
+        }
+
+        same(top) {
+            return _.difference(this.nids, top.nids).length === 0 &&
+                _.isEqual(this.addresses, top.addresses) &&
+                this.clusterVersion === top.clusterVersion &&
+                this.active === top.active;
+        }
+    }
+
+    /**
+     * Connected agents manager.
+     */
+    class AgentsHandler {
+        /**
+         * @constructor
+         */
+        constructor() {
+            /**
+             * Connected agents.
+             * @type {AgentSockets}
+             */
+            this._agentSockets = new AgentSockets();
+
+            this.clusters = [];
+            this.topLsnrs = [];
+        }
+
+        /**
+         * Collect supported agents list.
+         * @private
+         */
+        _collectSupportedAgents() {
+            const jarFilter = (file) => path.extname(file) === '.jar';
+
+            const agentArchives = fs.readdirSync(settings.agent.dists)
+                .filter((file) => path.extname(file) === '.zip');
+
+            const agentsPromises = _.map(agentArchives, (fileName) => {
+                const filePath = path.join(settings.agent.dists, fileName);
+
+                return JSZip.loadAsync(fs.readFileSync(filePath))
+                    .then((zip) => {
+                        const jarPath = _.find(_.keys(zip.files), jarFilter);
+
+                        return JSZip.loadAsync(zip.files[jarPath].async('nodebuffer'))
+                            .then((jar) => jar.files['META-INF/MANIFEST.MF'].async('string'))
+                            .then((lines) =>
+                                _.reduce(lines.split(/\s*\n+\s*/), (acc, line) => {
+                                    if (!_.isEmpty(line)) {
+                                        const arr = line.split(/\s*:\s*/);
+
+                                        acc[arr[0]] = arr[1];
+                                    }
+
+                                    return acc;
+                                }, {}))
+                            .then((manifest) => {
+                                const ver = manifest['Implementation-Version'];
+                                const buildTime = manifest['Build-Time'];
+
+                                if (ver && buildTime)
+                                    return { fileName, filePath, ver, buildTime };
+                            });
+                    });
+            });
+
+            return Promise.all(agentsPromises)
+                .then((descs) => {
+                    const agentDescs = _.keyBy(_.remove(descs, null), 'ver');
+
+                    const latestVer = _.head(Object.keys(agentDescs).sort((a, b) => {
+                        const aParts = a.split('.');
+                        const bParts = b.split('.');
+
+                        for (let i = 0; i < aParts.length; ++i) {
+                            if (aParts[i] !== bParts[i])
+                                return aParts[i] < bParts[i] ? 1 : -1;
+                        }
+
+                        if (aParts.length === bParts.length)
+                            return 0;
+
+                        return aParts.length < bParts.length ? 1 : -1;
+                    }));
+
+                    // Latest version of agent distribution.
+                    if (latestVer)
+                        agentDescs.current = agentDescs[latestVer];
+
+                    return agentDescs;
+                });
+        }
+
+        getOrCreateCluster(top) {
+            let cluster = _.find(this.clusters, (c) => c.isSameCluster(top));
+
+            if (_.isNil(cluster)) {
+                cluster = new Cluster(top);
+
+                this.clusters.push(cluster);
+            }
+
+            return cluster;
+        }
+
+        /**
+         * Add topology listener.
+         *
+         * @param lsnr
+         */
+        addTopologyListener(lsnr) {
+            this.topLsnrs.push(lsnr);
+        }
+
+        /**
+         * Link agent with browsers by account.
+         *
+         * @param {Socket} sock
+         * @param {Array.<mongo.Account>} accounts
+         * @param {Array.<String>} tokens
+         * @param {boolean} demoEnabled
+         *
+         * @private
+         */
+        onConnect(sock, accounts, tokens, demoEnabled) {
+            const agentSocket = new AgentSocket(sock, accounts, tokens, demoEnabled);
+
+            _.forEach(accounts, (account) => {
+                this._agentSockets.add(account, agentSocket);
+
+                this._browsersHnd.agentStats(account);
+            });
+
+            sock.on('disconnect', () => {
+                _.forEach(accounts, (account) => {
+                    _.pull(this._agentSockets.get(account), agentSocket);
+
+                    this._browsersHnd.agentStats(account);
+                });
+            });
+
+            sock.on('cluster:topology', (top) => {
+                if (_.isNil(top)) {
+                    console.log('Invalid format of message: "cluster:topology"');
+
+                    return;
+                }
+
+                const cluster = this.getOrCreateCluster(top);
+
+                _.forEach(this.topLsnrs, (lsnr) => lsnr(agentSocket, cluster, top));
+
+                if (agentSocket.cluster !== cluster) {
+                    agentSocket.cluster = cluster;
+
+                    _.forEach(accounts, (account) => {
+                        this._browsersHnd.agentStats(account);
+                    });
+                }
+                else {
+                    const changed = !cluster.same(top);
+
+                    if (changed) {
+                        cluster.update(top);
+
+                        _.forEach(accounts, (account) => {
+                            this._browsersHnd.clusterChanged(account, cluster);
+                        });
+                    }
+                }
+            });
+
+            sock.on('cluster:disconnected', () => {
+                const newTop = _.assign({}, agentSocket.cluster, {nids: []});
+
+                _.forEach(this.topLsnrs, (lsnr) => lsnr(agentSocket, agentSocket.cluster, newTop));
+
+                agentSocket.cluster = null;
+
+                _.forEach(accounts, (account) => {
+                    this._browsersHnd.agentStats(account);
+                });
+            });
+
+            return agentSocket;
+        }
+
+        getAccounts(tokens) {
+            return mongo.Account.find({token: {$in: tokens}}, '_id token').lean().exec()
+                .then((accounts) => ({accounts, activeTokens: _.uniq(_.map(accounts, 'token'))}));
+        }
+
+        /**
+         * @param {http.Server|https.Server} srv Server instance that we want to attach agent handler.
+         * @param {BrowsersHandler} browsersHnd
+         */
+        attach(srv, browsersHnd) {
+            this._browsersHnd = browsersHnd;
+
+            this._collectSupportedAgents()
+                .then((supportedAgents) => {
+                    this.currentAgent = _.get(supportedAgents, 'current');
+
+                    if (this.io)
+                        throw 'Agent server already started!';
+
+                    this.io = socketio(srv, {path: '/agents'});
+
+                    this.io.on('connection', (sock) => {
+                        const sockId = sock.id;
+
+                        console.log('Connected agent with socketId: ', sockId);
+
+                        sock.on('disconnect', (reason) => {
+                            console.log(`Agent disconnected with [socketId=${sockId}, reason=${reason}]`);
+                        });
+
+                        sock.on('agent:auth', ({ver, bt, tokens, disableDemo} = {}, cb) => {
+                            console.log(`Received authentication request [socketId=${sockId}, tokens=${tokens}, ver=${ver}].`);
+
+                            if (_.isEmpty(tokens))
+                                return cb('Tokens not set. Please reload agent archive or check settings');
+
+                            if (ver && bt && !_.isEmpty(supportedAgents)) {
+                                const btDistr = _.get(supportedAgents, [ver, 'buildTime']);
+
+                                if (_.isEmpty(btDistr) || btDistr !== bt)
+                                    return cb('You are using an older version of the agent. Please reload agent');
+                            }
+
+                            return this.getAccounts(tokens)
+                                .then(({accounts, activeTokens}) => {
+                                    if (_.isEmpty(activeTokens))
+                                        return cb(`Failed to authenticate with token(s): ${tokens.join(',')}. Please reload agent archive or check settings`);
+
+                                    cb(null, activeTokens);
+
+                                    return this.onConnect(sock, accounts, activeTokens, !disableDemo);
+                                })
+                                // TODO IGNITE-1379 send error to web master.
+                                .catch(() => cb(`Invalid token(s): ${tokens.join(',')}. Please reload agent archive or check settings`));
+                        });
+                    });
+                })
+                .catch(() => {
+                    console.log('Failed to collect supported agents');
+                });
+        }
+
+        agent(account, demo, clusterId) {
+            if (!this.io)
+                return Promise.reject(new Error('Agent server not started yet!'));
+
+            const socks = this._agentSockets.get(account);
+
+            if (_.isEmpty(socks))
+                return Promise.reject(new Error('Failed to find connected agent for this account'));
+
+            if (demo) {
+                const sock = _.find(socks, (sock) => sock.demo.enabled);
+
+                if (sock)
+                    return Promise.resolve(sock);
+
+                return Promise.reject(new Error('Demo mode disabled by administrator'));
+            }
+
+            if (_.isNil(clusterId))
+                return Promise.resolve(_.head(socks));
+
+            const sock = _.find(socks, (agentSock) => _.get(agentSock, 'cluster.id') === clusterId);
+
+            if (_.isEmpty(sock))
+                return Promise.reject(new Error('Failed to find agent connected to cluster'));
+
+            return Promise.resolve(sock);
+        }
+
+        agents(account) {
+            if (!this.io)
+                return Promise.reject(new Error('Agent server not started yet!'));
+
+            const socks = this._agentSockets.get(account);
+
+            if (_.isEmpty(socks))
+                return Promise.reject(new Error('Failed to find connected agent for this token'));
+
+            return Promise.resolve(socks);
+        }
+
+        /**
+         * Try stop agent for token if not used by others.
+         *
+         * @param {mongo.Account} account
+         */
+        onTokenReset(account) {
+            if (_.isNil(this.io))
+                return;
+
+            const agentSockets = this._agentSockets.get(account);
+
+            _.forEach(agentSockets, (sock) => sock.resetToken(account.token));
+        }
+    }
+
+    return new AgentsHandler();
+};
diff --git a/modules/backend/app/apiServer.js b/modules/backend/app/apiServer.js
new file mode 100644
index 0000000..90c39ba
--- /dev/null
+++ b/modules/backend/app/apiServer.js
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+
+// Fire me up!
+
+const Express = require('express');
+
+module.exports = {
+    implements: 'api-server',
+    inject: ['settings', 'configure', 'routes'],
+    factory(settings, configure, routes) {
+        /**
+         * Connected agents manager.
+         */
+        class ApiServer {
+            /**
+             * @param {Server} srv
+             */
+            attach(srv) {
+                const app = new Express();
+
+                configure.express(app);
+
+                routes.register(app);
+
+                if (settings.packaged) {
+                    const staticDir = path.join(process.cwd(), 'libs/frontend');
+
+                    try {
+                        fs.accessSync(staticDir, fs.F_OK);
+
+                        app.use('/', Express.static(staticDir));
+
+                        app.get('*', function(req, res) {
+                            res.sendFile(path.join(staticDir, 'index.html'));
+                        });
+                    }
+                    catch (e) {
+                        console.log(`Failed to find folder with frontend files: ${staticDir}`);
+                    }
+                }
+
+                // Catch 404 and forward to error handler.
+                app.use((req, res) => {
+                    if (req.xhr)
+                        return res.status(404).send({ error: 'Not Found: ' + req.originalUrl });
+
+                    return res.sendStatus(404);
+                });
+
+                // Production error handler: no stacktraces leaked to user.
+                app.use((err, req, res) => {
+                    res.status(err.status || 500);
+
+                    res.render('error', {
+                        message: err.message,
+                        error: {}
+                    });
+                });
+
+                srv.addListener('request', app);
+
+                return app;
+            }
+        }
+
+        return new ApiServer();
+    }
+};
diff --git a/modules/backend/app/browsersHandler.js b/modules/backend/app/browsersHandler.js
new file mode 100644
index 0000000..4d5b02d
--- /dev/null
+++ b/modules/backend/app/browsersHandler.js
@@ -0,0 +1,348 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const _ = require('lodash');
+const socketio = require('socket.io');
+
+// Fire me up!
+
+/**
+ * Module interaction with browsers.
+ */
+module.exports = {
+    implements: 'browsers-handler',
+    inject: ['configure', 'errors', 'mongo'],
+    factory: (configure, errors, mongo) => {
+        class BrowserSockets {
+            constructor() {
+                this.sockets = new Map();
+            }
+
+            /**
+             * @param {Socket} sock
+             */
+            add(sock) {
+                const key = sock.request.user._id.toString();
+
+                if (this.sockets.has(key))
+                    this.sockets.get(key).push(sock);
+                else
+                    this.sockets.set(key, [sock]);
+
+                return this.sockets.get(sock.request.user);
+            }
+
+            /**
+             * @param {Socket} sock
+             */
+            remove(sock) {
+                const key = sock.request.user._id.toString();
+
+                const sockets = this.sockets.get(key);
+
+                _.pull(sockets, sock);
+
+                return sockets;
+            }
+
+            get(account) {
+                const key = account._id.toString();
+
+                let sockets = this.sockets.get(key);
+
+                if (_.isEmpty(sockets))
+                    this.sockets.set(key, sockets = []);
+
+                return sockets;
+            }
+        }
+
+        class BrowsersHandler {
+            /**
+             * @constructor
+             */
+            constructor() {
+                /**
+                 * Connected browsers.
+                 * @type {BrowserSockets}
+                 */
+                this._browserSockets = new BrowserSockets();
+
+                /**
+                 * Registered Visor task.
+                 * @type {Map}
+                 */
+                this._visorTasks = new Map();
+            }
+
+            /**
+             * @param {Error} err
+             * @return {{code: number, message: *}}
+             */
+            errorTransformer(err) {
+                return {
+                    code: err.code || 1,
+                    message: err.message || err
+                };
+            }
+
+            /**
+             * @param {String} account
+             * @param {Array.<Socket>} [socks]
+             */
+            agentStats(account, socks = this._browserSockets.get(account)) {
+                return this._agentHnd.agents(account)
+                    .then((agentSocks) => {
+                        const stat = _.reduce(agentSocks, (acc, agentSock) => {
+                            acc.count += 1;
+                            acc.hasDemo = acc.hasDemo || _.get(agentSock, 'demo.enabled');
+
+                            if (agentSock.cluster)
+                                acc.clusters.push(agentSock.cluster);
+
+                            return acc;
+                        }, {count: 0, hasDemo: false, clusters: []});
+
+                        stat.clusters = _.uniqWith(stat.clusters, _.isEqual);
+
+                        return stat;
+                    })
+                    .catch(() => ({count: 0, hasDemo: false, clusters: []}))
+                    .then((stat) => _.forEach(socks, (sock) => sock.emit('agents:stat', stat)));
+            }
+
+            clusterChanged(account, cluster) {
+                const socks = this._browserSockets.get(account);
+
+                _.forEach(socks, (sock) => {
+                    if (sock)
+                        sock.emit('cluster:changed', cluster);
+                    else
+                        console.log(`Fount closed socket [account=${account}, cluster=${cluster}]`);
+                });
+            }
+
+            pushInitialData(sock) {
+                // Send initial data.
+            }
+
+            emitNotification(sock) {
+                sock.emit('user:notifications', this.notification);
+            }
+
+            /**
+             * @param {String} notification Notification message.
+             */
+            updateNotification(notification) {
+                this.notification = notification;
+
+                for (const socks of this._browserSockets.sockets.values()) {
+                    for (const sock of socks)
+                        this.emitNotification(sock);
+                }
+            }
+
+            executeOnAgent(account, demo, event, ...args) {
+                const cb = _.last(args);
+
+                return this._agentHnd.agent(account, demo)
+                    .then((agentSock) => agentSock.emitEvent(event, ..._.dropRight(args)))
+                    .then((res) => cb(null, res))
+                    .catch((err) => cb(this.errorTransformer(err)));
+            }
+
+            agentListeners(sock) {
+                const demo = sock.request._query.IgniteDemoMode === 'true';
+                const account = () => sock.request.user;
+
+                // Return available drivers to browser.
+                sock.on('schemaImport:drivers', (...args) => {
+                    this.executeOnAgent(account(), demo, 'schemaImport:drivers', ...args);
+                });
+
+                // Return schemas from database to browser.
+                sock.on('schemaImport:schemas', (...args) => {
+                    this.executeOnAgent(account(), demo, 'schemaImport:schemas', ...args);
+                });
+
+                // Return tables from database to browser.
+                sock.on('schemaImport:metadata', (...args) => {
+                    this.executeOnAgent(account(), demo, 'schemaImport:metadata', ...args);
+                });
+            }
+
+            /**
+             * @param {Promise.<AgentSocket>} agent
+             * @param {Boolean} demo
+             * @param {{sessionId: String}|{'login': String, 'password': String}} credentials
+             * @param {Object.<String, String>} params
+             * @return {Promise.<T>}
+             */
+            executeOnNode(agent, token, demo, credentials, params) {
+                return agent
+                    .then((agentSock) => agentSock.emitEvent('node:rest',
+                        {uri: 'ignite', token, demo, params: _.merge({}, credentials, params)}));
+            }
+
+            registerVisorTask(taskId, taskCls, ...argCls) {
+                this._visorTasks.set(taskId, {
+                    taskCls,
+                    argCls
+                });
+            }
+
+            nodeListeners(sock) {
+                // Return command result from grid to browser.
+                sock.on('node:rest', (arg, cb) => {
+                    const {clusterId, params, credentials} = arg || {};
+
+                    if (!_.isFunction(cb))
+                        cb = console.log;
+
+                    const demo = _.get(sock, 'request._query.IgniteDemoMode') === 'true';
+
+                    if ((_.isNil(clusterId) && !demo) || _.isNil(params)) {
+                        console.log('Received invalid message: "node:rest" on socket:', JSON.stringify(sock.handshake));
+
+                        return cb('Invalid format of message: "node:rest"');
+                    }
+
+                    const agent = this._agentHnd.agent(sock.request.user, demo, clusterId);
+
+                    const token = sock.request.user.token;
+
+                    this.executeOnNode(agent, token, demo, credentials, params)
+                        .then((data) => cb(null, data))
+                        .catch((err) => cb(this.errorTransformer(err)));
+                });
+
+                const internalVisor = (postfix) => `org.apache.ignite.internal.visor.${postfix}`;
+
+                this.registerVisorTask('querySql', internalVisor('query.VisorQueryTask'), internalVisor('query.VisorQueryArg'));
+                this.registerVisorTask('querySqlV2', internalVisor('query.VisorQueryTask'), internalVisor('query.VisorQueryArgV2'));
+                this.registerVisorTask('querySqlV3', internalVisor('query.VisorQueryTask'), internalVisor('query.VisorQueryArgV3'));
+                this.registerVisorTask('querySqlX2', internalVisor('query.VisorQueryTask'), internalVisor('query.VisorQueryTaskArg'));
+
+                this.registerVisorTask('queryScanX2', internalVisor('query.VisorScanQueryTask'), internalVisor('query.VisorScanQueryTaskArg'));
+
+                this.registerVisorTask('queryFetch', internalVisor('query.VisorQueryNextPageTask'), 'org.apache.ignite.lang.IgniteBiTuple', 'java.lang.String', 'java.lang.Integer');
+                this.registerVisorTask('queryFetchX2', internalVisor('query.VisorQueryNextPageTask'), internalVisor('query.VisorQueryNextPageTaskArg'));
+
+                this.registerVisorTask('queryFetchFirstPage', internalVisor('query.VisorQueryFetchFirstPageTask'), internalVisor('query.VisorQueryNextPageTaskArg'));
+                this.registerVisorTask('queryPing', internalVisor('query.VisorQueryPingTask'), internalVisor('query.VisorQueryNextPageTaskArg'));
+
+                this.registerVisorTask('queryClose', internalVisor('query.VisorQueryCleanupTask'), 'java.util.Map', 'java.util.UUID', 'java.util.Set');
+                this.registerVisorTask('queryCloseX2', internalVisor('query.VisorQueryCleanupTask'), internalVisor('query.VisorQueryCleanupTaskArg'));
+
+                this.registerVisorTask('toggleClusterState', internalVisor('misc.VisorChangeGridActiveStateTask'), internalVisor('misc.VisorChangeGridActiveStateTaskArg'));
+
+                this.registerVisorTask('cacheNamesCollectorTask', internalVisor('cache.VisorCacheNamesCollectorTask'), 'java.lang.Void');
+
+                this.registerVisorTask('cacheNodesTask', internalVisor('cache.VisorCacheNodesTask'), 'java.lang.String');
+                this.registerVisorTask('cacheNodesTaskX2', internalVisor('cache.VisorCacheNodesTask'), internalVisor('cache.VisorCacheNodesTaskArg'));
+
+                // Return command result from grid to browser.
+                sock.on('node:visor', (arg, cb) => {
+                    const {clusterId, params, credentials} = arg || {};
+
+                    if (!_.isFunction(cb))
+                        cb = console.log;
+
+                    const demo = _.get(sock, 'request._query.IgniteDemoMode') === 'true';
+
+                    if ((_.isNil(clusterId) && !demo) || _.isNil(params)) {
+                        console.log('Received invalid message: "node:visor" on socket:', JSON.stringify(sock.handshake));
+
+                        return cb('Invalid format of message: "node:visor"');
+                    }
+
+                    const {taskId, nids, args = []} = params;
+
+                    const desc = this._visorTasks.get(taskId);
+
+                    if (_.isNil(desc))
+                        return cb(this.errorTransformer(new errors.IllegalArgumentException(`Failed to find Visor task for id: ${taskId}`)));
+
+                    const exeParams = {
+                        cmd: 'exe',
+                        name: 'org.apache.ignite.internal.visor.compute.VisorGatewayTask',
+                        p1: nids,
+                        p2: desc.taskCls
+                    };
+
+                    _.forEach(_.concat(desc.argCls, args), (param, idx) => { exeParams[`p${idx + 3}`] = param; });
+
+                    const agent = this._agentHnd.agent(sock.request.user, demo, clusterId);
+
+                    const token = sock.request.user.token;
+
+                    this.executeOnNode(agent, token, demo, credentials, exeParams)
+                        .then((data) => {
+                            if (data.finished && !data.zipped)
+                                return cb(null, data.result);
+
+                            return cb(null, data);
+                        })
+                        .catch((err) => cb(this.errorTransformer(err)));
+                });
+            }
+
+            /**
+             *
+             * @param server
+             * @param {AgentsHandler} agentHnd
+             */
+            attach(server, agentHnd) {
+                this._agentHnd = agentHnd;
+
+                if (this.io)
+                    throw 'Browser server already started!';
+
+                mongo.Notifications.findOne().sort('-date').exec()
+                    .then((notification) => {
+                        this.notification = notification;
+                    })
+                    .then(() => {
+                        const io = socketio(server);
+
+                        configure.socketio(io);
+
+                        // Handle browser connect event.
+                        io.sockets.on('connection', (sock) => {
+                            this._browserSockets.add(sock);
+
+                            // Handle browser disconnect event.
+                            sock.on('disconnect', () => {
+                                this._browserSockets.remove(sock);
+                            });
+
+                            this.agentListeners(sock);
+                            this.nodeListeners(sock);
+
+                            this.pushInitialData(sock);
+                            this.agentStats(sock.request.user, [sock]);
+                            this.emitNotification(sock);
+                        });
+                    });
+            }
+        }
+
+        return new BrowsersHandler();
+    }
+};
diff --git a/modules/backend/app/configure.js b/modules/backend/app/configure.js
new file mode 100644
index 0000000..b907a1e
--- /dev/null
+++ b/modules/backend/app/configure.js
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const _ = require('lodash');
+const logger = require('morgan');
+const cookieParser = require('cookie-parser');
+const bodyParser = require('body-parser');
+const session = require('express-session');
+const MongoDBStore = require('connect-mongodb-session');
+const passport = require('passport');
+const passportSocketIo = require('passport.socketio');
+const mongoSanitize = require('express-mongo-sanitize');
+
+// Fire me up!
+
+/**
+ * Module for configuration express and websocket server.
+ */
+module.exports = {
+    implements: 'configure',
+    inject: ['settings', 'mongo', 'middlewares:*']
+};
+
+module.exports.factory = function(settings, mongo, apis) {
+    const _sessionStore = new (MongoDBStore(session))({uri: settings.mongoUrl});
+
+    return {
+        express: (app) => {
+            app.use(logger('dev', {
+                skip: (req, res) => res.statusCode < 400
+            }));
+
+            _.forEach(apis, (api) => app.use(api));
+
+            app.use(bodyParser.json({limit: '50mb'}));
+            app.use(bodyParser.urlencoded({limit: '50mb', extended: true}));
+
+
+            app.use(mongoSanitize({replaceWith: '_'}));
+
+            app.use(session({
+                secret: settings.sessionSecret,
+                resave: false,
+                saveUninitialized: true,
+                unset: 'destroy',
+                cookie: {
+                    expires: new Date(Date.now() + settings.cookieTTL),
+                    maxAge: settings.cookieTTL
+                },
+                store: _sessionStore
+            }));
+
+            app.use(passport.initialize());
+            app.use(passport.session());
+
+            passport.serializeUser((user, done) => done(null, user._id));
+
+            passport.deserializeUser((id, done) => {
+                if (mongo.ObjectId.isValid(id))
+                    return mongo.Account.findById(id, done);
+
+                // Invalidates the existing login session.
+                done(null, false);
+            });
+
+            passport.use(mongo.Account.createStrategy());
+        },
+        socketio: (io) => {
+            const _onAuthorizeSuccess = (data, accept) => accept();
+
+            const _onAuthorizeFail = (data, message, error, accept) => {
+                if (error)
+                    accept(new Error(message));
+
+                return accept(new Error(message));
+            };
+
+            io.use(passportSocketIo.authorize({
+                cookieParser,
+                key: 'connect.sid', // the name of the cookie where express/connect stores its session_id
+                secret: settings.sessionSecret, // the session_secret to parse the cookie
+                store: _sessionStore, // we NEED to use a sessionstore. no memorystore please
+                success: _onAuthorizeSuccess, // *optional* callback on success - read more below
+                fail: _onAuthorizeFail // *optional* callback on fail/error - read more below
+            }));
+        }
+    };
+};
diff --git a/modules/backend/app/mongo.js b/modules/backend/app/mongo.js
new file mode 100644
index 0000000..b9ee9de
--- /dev/null
+++ b/modules/backend/app/mongo.js
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const fs = require('fs');
+const _ = require('lodash');
+const mongoose = require('mongoose');
+
+// Fire me up!
+
+/**
+ * Module mongo schema.
+ */
+module.exports = {
+    implements: 'mongo',
+    inject: ['settings', 'schemas']
+};
+
+const defineSchema = (schemas) => {
+    const result = { connection: mongoose.connection };
+
+    result.ObjectId = mongoose.Types.ObjectId;
+
+    result.errCodes = {
+        DUPLICATE_KEY_ERROR: 11000,
+        DUPLICATE_KEY_UPDATE_ERROR: 11001
+    };
+
+    // Define models.
+    _.forEach(schemas, (schema, name) => {
+        result[name] = mongoose.model(name, schema);
+    });
+
+    result.handleError = function(res, err) {
+        // TODO IGNITE-843 Send error to admin
+        res.status(err.code || 500).send(err.message);
+    };
+
+    return result;
+};
+
+const upgradeAccounts = (mongo, activation) => {
+    if (activation) {
+        return mongo.Account.find({
+            $or: [{activated: false}, {activated: {$exists: false}}],
+            activationToken: {$exists: false}
+        }, '_id').lean().exec()
+            .then((accounts) => {
+                const conditions = _.map(accounts, (account) => ({session: {$regex: `"${account._id}"`}}));
+
+                return mongoose.connection.db.collection('sessions').deleteMany({$or: conditions});
+            });
+    }
+
+    return mongo.Account.updateMany({activated: false}, {$unset: {activationSentAt: '', activationToken: ''}}).exec();
+};
+
+module.exports.factory = function(settings, schemas) {
+    // Use native promises
+    mongoose.Promise = global.Promise;
+
+    console.log(settings.mongoUrl, 'Trying to connect to local MongoDB...');
+
+    // Connect to mongoDB database.
+    return mongoose.connect(settings.mongoUrl, {useNewUrlParser: true, useCreateIndex: true})
+        .then(() => defineSchema(schemas))
+        .catch(() => {
+            console.log(`Failed to connect to MongoDB with connection string: "${settings.mongoUrl}", will try to download and start embedded MongoDB`);
+
+            const dbDir = `${process.cwd()}/user_data`;
+
+            if (!fs.existsSync(dbDir))
+                fs.mkdirSync(dbDir);
+
+            const {MongodHelper} = require('mongodb-prebuilt');
+            const {MongoDBDownload} = require('mongodb-download');
+
+            const helper = new MongodHelper(['--port', '27017', '--dbpath', dbDir]);
+
+            helper.mongoBin.mongoDBPrebuilt.mongoDBDownload = new MongoDBDownload({
+                downloadDir: `${process.cwd()}/libs/mongodb`,
+                version: '4.0.9'
+            });
+
+            let mongodRun;
+
+            if (settings.packaged) {
+                mongodRun = new Promise((resolve, reject) => {
+                    helper.resolveLink = resolve;
+                    helper.rejectLink = reject;
+
+                    helper.mongoBin.runCommand()
+                        .then(() => {
+                            helper.mongoBin.childProcess.removeAllListeners('close');
+
+                            helper.mongoBin.childProcess.stderr.on('data', (data) => helper.stderrHandler(data));
+                            helper.mongoBin.childProcess.stdout.on('data', (data) => helper.stdoutHandler(data));
+                            helper.mongoBin.childProcess.on('close', (code) => helper.closeHandler(code));
+                        });
+                });
+            }
+            else
+                mongodRun = helper.run();
+
+            return mongodRun
+                .catch((err) => {
+                    console.log('Failed to start embedded MongoDB', err);
+
+                    return Promise.reject(err);
+                })
+                .then(() => {
+                    console.log('Embedded MongoDB successfully started');
+
+                    return mongoose.connect(settings.mongoUrl, {useNewUrlParser: true, useCreateIndex: true})
+                        .catch((err) => {
+                            console.log('Failed to connect to embedded MongoDB', err);
+
+                            return Promise.reject(err);
+                        });
+                })
+                .then(() => defineSchema(schemas))
+                .then((mongo) => {
+                    if (settings.packaged) {
+                        return mongo.Account.count()
+                            .then((count) => {
+                                if (count === 0) {
+                                    return Promise.all([
+                                        mongo.Account.create({
+                                            _id: '59fc0c25e145c32be0f83b33',
+                                            salt: '7b4ccb9e375508a8f87c8f347083ce98cb8785d857dd18208f9a480e992a26bb',
+                                            hash: '909d5ed6e0b0a656ef542e2e8e851e9eb00cfb77984e0a6b4597c335d1436a577b3b289601eb8d1f3646e488cd5ea2bbb3e97fcc131cd6a9571407a45b1817bf1af1dd0ccdd070f07733da19e636ff9787369c5f38f86075f78c60809fe4a52288a68ca38aae0ad2bd0cc77b4cae310abf260e9523d361fd9be60e823a7d8e73954ddb18091e668acd3f57baf9fa7db4267e198d829761997a4741734335589ab62793ceb089e8fffe6e5b0e86f332b33a3011ba44e6efd29736f31cbd2b2023e5173baf517f337eb7a4321ea2b67ec827cffa271d26d3f2def93b5efa3ae7e6e327e55feb121ee96b8ff5016527cc7d854a9b49b44c993387c1093705cb26b1802a2e4c1d34508fb93d051d7e5e2e6cc65b6048a999f94c369973b46b204295f0b2f23f8e30723f9e984ddb2c53dcbf0a77a6d0795d44c3ad97a4ae49d6767db9630e2ef76c2069da87088f1400b1292df9bd787122b2cfef1f26a884a298a0bab3d6e6b689381cf6389d2f019e6cd19e82c84048bacfdd1bee946f9d40dda040be426e583abf92529a1c4f032d5058a9799a77e6642312b8d231d79300d5d0d3f74d62797f9d192e8581698e9539812a539ef1b9fbf718f44dd549896ea9449f6ea744586222e5fc29dfcd5eb79e7646ad3d37868f5073833c554853dee6b067bf2bbfab44c011f2de98a8570292f8109b6bde11e3be51075a656c32b521b7',
+                                            email: 'admin@admin',
+                                            firstName: 'admin',
+                                            lastName: 'admin',
+                                            company: 'admin',
+                                            country: 'United States',
+                                            admin: true,
+                                            token: 'ruQvlWff09zqoVYyh6WJ',
+                                            attempts: 0,
+                                            resetPasswordToken: 'O2GWgOkKkhqpDcxjYnSP',
+                                            activated: true
+                                        }),
+                                        mongo.Space.create({
+                                            _id: '59fc0c26e145c32be0f83b34',
+                                            name: 'Personal space',
+                                            owner: '59fc0c25e145c32be0f83b33',
+                                            usedBy: [],
+                                            demo: false
+                                        })
+                                    ]);
+                                }
+                            })
+                            .then(() => mongo)
+                            .catch(() => mongo);
+                    }
+
+                    return mongo;
+                });
+        })
+        .then((mongo) => {
+            return upgradeAccounts(mongo, settings.activation.enabled)
+                .then(() => mongo)
+                .catch(() => mongo);
+        });
+};
diff --git a/modules/backend/app/nconf.js b/modules/backend/app/nconf.js
new file mode 100644
index 0000000..3f5d7d1
--- /dev/null
+++ b/modules/backend/app/nconf.js
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const fs = require('fs');
+const nconf = require('nconf');
+
+// Fire me up!
+
+/**
+ * Module with server-side configuration.
+ */
+module.exports = {
+    implements: 'nconf',
+    factory() {
+        nconf.env({separator: '_'}).argv();
+
+        const dfltFile = 'config/settings.json';
+        const customFile = nconf.get('settings') || dfltFile;
+
+        try {
+            fs.accessSync(customFile, fs.F_OK);
+
+            nconf.file({file: customFile});
+        }
+        catch (ignored) {
+            try {
+                fs.accessSync(dfltFile, fs.F_OK);
+
+                nconf.file({file: dfltFile});
+            }
+            catch (ignored2) {
+                // No-op.
+            }
+        }
+
+        return nconf;
+    }
+};
diff --git a/modules/backend/app/routes.js b/modules/backend/app/routes.js
new file mode 100644
index 0000000..ce7b5d8
--- /dev/null
+++ b/modules/backend/app/routes.js
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'routes',
+    inject: ['routes/public', 'routes/admin', 'routes/profiles', 'routes/demo', 'routes/clusters', 'routes/domains',
+        'routes/caches', 'routes/igfss', 'routes/notebooks', 'routes/downloads', 'routes/configurations', 'routes/activities']
+};
+
+module.exports.factory = function(publicRoute, adminRoute, profilesRoute, demoRoute,
+    clustersRoute, domainsRoute, cachesRoute, igfssRoute, notebooksRoute, downloadsRoute, configurationsRoute, activitiesRoute) {
+    return {
+        register: (app) => {
+            const _mustAuthenticated = (req, res, next) => {
+                if (req.isAuthenticated())
+                    return next();
+
+                res.status(401).send('Access denied. You are not authorized to access this page.');
+            };
+
+            const _adminOnly = (req, res, next) => {
+                if (req.isAuthenticated() && req.user.admin)
+                    return next();
+
+                res.status(401).send('Access denied. You are not authorized to access this page.');
+            };
+
+            // Registering the standard routes.
+            // NOTE: Order is important!
+            app.use('/api/v1/', publicRoute);
+            app.use('/api/v1/admin', _mustAuthenticated, _adminOnly, adminRoute);
+            app.use('/api/v1/profile', _mustAuthenticated, profilesRoute);
+            app.use('/api/v1/demo', _mustAuthenticated, demoRoute);
+
+            app.all('/api/v1/configuration/*', _mustAuthenticated);
+
+            app.use('/api/v1/configuration/clusters', clustersRoute);
+            app.use('/api/v1/configuration/domains', domainsRoute);
+            app.use('/api/v1/configuration/caches', cachesRoute);
+            app.use('/api/v1/configuration/igfs', igfssRoute);
+            app.use('/api/v1/configuration', configurationsRoute);
+
+            app.use('/api/v1/notebooks', _mustAuthenticated, notebooksRoute);
+            app.use('/api/v1/downloads', _mustAuthenticated, downloadsRoute);
+            app.use('/api/v1/activities', _mustAuthenticated, activitiesRoute);
+        }
+    };
+};
diff --git a/modules/backend/app/schemas.js b/modules/backend/app/schemas.js
new file mode 100644
index 0000000..21c8120
--- /dev/null
+++ b/modules/backend/app/schemas.js
@@ -0,0 +1,1323 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const mongoose = require('mongoose');
+const passportMongo = require('passport-local-mongoose');
+
+// Fire me up!
+
+/**
+ * Module mongo schema.
+ */
+module.exports = {
+    implements: 'schemas',
+    inject: []
+};
+
+module.exports.factory = function() {
+    const Schema = mongoose.Schema;
+    const ObjectId = mongoose.Schema.Types.ObjectId;
+
+    // Define Account schema.
+    const Account = new Schema({
+        firstName: String,
+        lastName: String,
+        email: {type: String, unique: true},
+        phone: String,
+        company: String,
+        country: String,
+        registered: Date,
+        lastLogin: Date,
+        lastActivity: Date,
+        admin: Boolean,
+        token: String,
+        resetPasswordToken: String,
+        activated: {type: Boolean, default: false},
+        activationSentAt: Date,
+        activationToken: String
+    });
+
+    // Install passport plugin.
+    Account.plugin(passportMongo, {
+        usernameField: 'email', limitAttempts: true, lastLoginField: 'lastLogin',
+        usernameLowerCase: true,
+        errorMessages: {
+            UserExistsError: 'A user with the given email is already registered'
+        }
+    });
+
+    const transform = (doc, ret) => {
+        return {
+            _id: ret._id,
+            email: ret.email,
+            phone: ret.phone,
+            firstName: ret.firstName,
+            lastName: ret.lastName,
+            company: ret.company,
+            country: ret.country,
+            admin: ret.admin,
+            token: ret.token,
+            registered: ret.registered,
+            lastLogin: ret.lastLogin,
+            lastActivity: ret.lastActivity
+        };
+    };
+
+    // Configure transformation to JSON.
+    Account.set('toJSON', {transform});
+
+    // Configure transformation to JSON.
+    Account.set('toObject', {transform});
+
+    // Define Space schema.
+    const Space = new Schema({
+        name: String,
+        owner: {type: ObjectId, ref: 'Account'},
+        demo: {type: Boolean, default: false},
+        usedBy: [{
+            permission: {type: String, enum: ['VIEW', 'FULL']},
+            account: {type: ObjectId, ref: 'Account'}
+        }]
+    });
+
+    // Define Domain model schema.
+    const DomainModel = new Schema({
+        space: {type: ObjectId, ref: 'Space', index: true, required: true},
+        clusters: [{type: ObjectId, ref: 'Cluster'}],
+        caches: [{type: ObjectId, ref: 'Cache'}],
+        queryMetadata: {type: String, enum: ['Annotations', 'Configuration']},
+        kind: {type: String, enum: ['query', 'store', 'both']},
+        tableName: String,
+        keyFieldName: String,
+        valueFieldName: String,
+        databaseSchema: String,
+        databaseTable: String,
+        keyType: String,
+        valueType: {type: String},
+        keyFields: [{
+            databaseFieldName: String,
+            databaseFieldType: String,
+            javaFieldName: String,
+            javaFieldType: String
+        }],
+        valueFields: [{
+            databaseFieldName: String,
+            databaseFieldType: String,
+            javaFieldName: String,
+            javaFieldType: String
+        }],
+        queryKeyFields: [String],
+        fields: [{
+            name: String,
+            className: String,
+            notNull: Boolean,
+            defaultValue: String,
+            precision: Number,
+            scale: Number
+        }],
+        aliases: [{field: String, alias: String}],
+        indexes: [{
+            name: String,
+            indexType: {type: String, enum: ['SORTED', 'FULLTEXT', 'GEOSPATIAL']},
+            fields: [{name: String, direction: Boolean}],
+            inlineSizeType: Number,
+            inlineSize: Number
+        }],
+        generatePojo: Boolean
+    });
+
+    DomainModel.index({valueType: 1, space: 1, clusters: 1}, {unique: true});
+
+    // Define Cache schema.
+    const Cache = new Schema({
+        space: {type: ObjectId, ref: 'Space', index: true, required: true},
+        name: {type: String},
+        groupName: {type: String},
+        clusters: [{type: ObjectId, ref: 'Cluster'}],
+        domains: [{type: ObjectId, ref: 'DomainModel'}],
+        cacheMode: {type: String, enum: ['PARTITIONED', 'REPLICATED', 'LOCAL']},
+        atomicityMode: {type: String, enum: ['ATOMIC', 'TRANSACTIONAL', 'TRANSACTIONAL_SNAPSHOT']},
+        partitionLossPolicy: {
+            type: String,
+            enum: ['READ_ONLY_SAFE', 'READ_ONLY_ALL', 'READ_WRITE_SAFE', 'READ_WRITE_ALL', 'IGNORE']
+        },
+
+        affinity: {
+            kind: {type: String, enum: ['Default', 'Rendezvous', 'Fair', 'Custom']},
+            Rendezvous: {
+                affinityBackupFilter: String,
+                partitions: Number,
+                excludeNeighbors: Boolean
+            },
+            Fair: {
+                affinityBackupFilter: String,
+                partitions: Number,
+                excludeNeighbors: Boolean
+            },
+            Custom: {
+                className: String
+            }
+        },
+
+        affinityMapper: String,
+
+        nodeFilter: {
+            kind: {type: String, enum: ['Default', 'Exclude', 'IGFS', 'OnNodes', 'Custom']},
+            Exclude: {
+                nodeId: String
+            },
+            IGFS: {
+                igfs: {type: ObjectId, ref: 'Igfs'}
+            },
+            Custom: {
+                className: String
+            }
+        },
+
+        backups: Number,
+        memoryMode: {type: String, enum: ['ONHEAP_TIERED', 'OFFHEAP_TIERED', 'OFFHEAP_VALUES']},
+        offHeapMode: Number,
+        offHeapMaxMemory: Number,
+        startSize: Number,
+        swapEnabled: Boolean,
+        cacheWriterFactory: String,
+        cacheLoaderFactory: String,
+        expiryPolicyFactory: String,
+        interceptor: String,
+        storeByValue: Boolean,
+        eagerTtl: {type: Boolean, default: true},
+        encryptionEnabled: Boolean,
+        eventsDisabled: Boolean,
+
+        keyConfiguration: [{
+            typeName: String,
+            affinityKeyFieldName: String
+        }],
+
+        cacheStoreSessionListenerFactories: [String],
+
+        onheapCacheEnabled: Boolean,
+
+        evictionPolicy: {
+            kind: {type: String, enum: ['LRU', 'FIFO', 'SORTED']},
+            LRU: {
+                batchSize: Number,
+                maxMemorySize: Number,
+                maxSize: Number
+            },
+            FIFO: {
+                batchSize: Number,
+                maxMemorySize: Number,
+                maxSize: Number
+            },
+            SORTED: {
+                batchSize: Number,
+                maxMemorySize: Number,
+                maxSize: Number
+            }
+        },
+
+        rebalanceMode: {type: String, enum: ['SYNC', 'ASYNC', 'NONE']},
+        rebalanceBatchSize: Number,
+        rebalanceBatchesPrefetchCount: Number,
+        rebalanceOrder: Number,
+        rebalanceDelay: Number,
+        rebalanceTimeout: Number,
+        rebalanceThrottle: Number,
+
+        cacheStoreFactory: {
+            kind: {
+                type: String,
+                enum: ['CacheJdbcPojoStoreFactory', 'CacheJdbcBlobStoreFactory', 'CacheHibernateBlobStoreFactory']
+            },
+            CacheJdbcPojoStoreFactory: {
+                dataSourceBean: String,
+                dialect: {
+                    type: String,
+                    enum: ['Generic', 'Oracle', 'DB2', 'SQLServer', 'MySQL', 'PostgreSQL', 'H2']
+                },
+                implementationVersion: String,
+                batchSize: Number,
+                maximumPoolSize: Number,
+                maximumWriteAttempts: Number,
+                parallelLoadCacheMinimumThreshold: Number,
+                hasher: String,
+                transformer: String,
+                sqlEscapeAll: Boolean
+            },
+            CacheJdbcBlobStoreFactory: {
+                connectVia: {type: String, enum: ['URL', 'DataSource']},
+                connectionUrl: String,
+                user: String,
+                dataSourceBean: String,
+                dialect: {
+                    type: String,
+                    enum: ['Generic', 'Oracle', 'DB2', 'SQLServer', 'MySQL', 'PostgreSQL', 'H2']
+                },
+                initSchema: Boolean,
+                createTableQuery: String,
+                loadQuery: String,
+                insertQuery: String,
+                updateQuery: String,
+                deleteQuery: String
+            },
+            CacheHibernateBlobStoreFactory: {
+                hibernateProperties: [{name: String, value: String}]
+            }
+        },
+        storeConcurrentLoadAllThreshold: Number,
+        maxQueryIteratorsCount: Number,
+        storeKeepBinary: Boolean,
+        loadPreviousValue: Boolean,
+        readThrough: Boolean,
+        writeThrough: Boolean,
+
+        writeBehindEnabled: Boolean,
+        writeBehindBatchSize: Number,
+        writeBehindFlushSize: Number,
+        writeBehindFlushFrequency: Number,
+        writeBehindFlushThreadCount: Number,
+        writeBehindCoalescing: {type: Boolean, default: true},
+
+        isInvalidate: Boolean,
+        defaultLockTimeout: Number,
+        atomicWriteOrderMode: {type: String, enum: ['CLOCK', 'PRIMARY']},
+        writeSynchronizationMode: {type: String, enum: ['FULL_SYNC', 'FULL_ASYNC', 'PRIMARY_SYNC']},
+
+        sqlEscapeAll: Boolean,
+        sqlSchema: String,
+        sqlOnheapRowCacheSize: Number,
+        longQueryWarningTimeout: Number,
+        sqlFunctionClasses: [String],
+        snapshotableIndex: Boolean,
+        queryDetailMetricsSize: Number,
+        queryParallelism: Number,
+        statisticsEnabled: Boolean,
+        managementEnabled: Boolean,
+        readFromBackup: Boolean,
+        copyOnRead: Boolean,
+        maxConcurrentAsyncOperations: Number,
+        nearConfiguration: {
+            enabled: Boolean,
+            nearStartSize: Number,
+            nearEvictionPolicy: {
+                kind: {type: String, enum: ['LRU', 'FIFO', 'SORTED']},
+                LRU: {
+                    batchSize: Number,
+                    maxMemorySize: Number,
+                    maxSize: Number
+                },
+                FIFO: {
+                    batchSize: Number,
+                    maxMemorySize: Number,
+                    maxSize: Number
+                },
+                SORTED: {
+                    batchSize: Number,
+                    maxMemorySize: Number,
+                    maxSize: Number
+                }
+            }
+        },
+        clientNearConfiguration: {
+            enabled: Boolean,
+            nearStartSize: Number,
+            nearEvictionPolicy: {
+                kind: {type: String, enum: ['LRU', 'FIFO', 'SORTED']},
+                LRU: {
+                    batchSize: Number,
+                    maxMemorySize: Number,
+                    maxSize: Number
+                },
+                FIFO: {
+                    batchSize: Number,
+                    maxMemorySize: Number,
+                    maxSize: Number
+                },
+                SORTED: {
+                    batchSize: Number,
+                    maxMemorySize: Number,
+                    maxSize: Number
+                }
+            }
+        },
+        evictionFilter: String,
+        memoryPolicyName: String,
+        dataRegionName: String,
+        sqlIndexMaxInlineSize: Number,
+        topologyValidator: String,
+        diskPageCompression: {type: String, enum: ['SKIP_GARBAGE', 'ZSTD', 'LZ4', 'SNAPPY']},
+        diskPageCompressionLevel: Number
+    });
+
+    Cache.index({name: 1, space: 1, clusters: 1}, {unique: true});
+
+    const Igfs = new Schema({
+        space: {type: ObjectId, ref: 'Space', index: true, required: true},
+        name: {type: String},
+        clusters: [{type: ObjectId, ref: 'Cluster'}],
+        affinnityGroupSize: Number,
+        blockSize: Number,
+        streamBufferSize: Number,
+        dataCacheName: String,
+        metaCacheName: String,
+        defaultMode: {type: String, enum: ['PRIMARY', 'PROXY', 'DUAL_SYNC', 'DUAL_ASYNC']},
+        dualModeMaxPendingPutsSize: Number,
+        dualModePutExecutorService: String,
+        dualModePutExecutorServiceShutdown: Boolean,
+        fragmentizerConcurrentFiles: Number,
+        fragmentizerEnabled: Boolean,
+        fragmentizerThrottlingBlockLength: Number,
+        fragmentizerThrottlingDelay: Number,
+        ipcEndpointConfiguration: {
+            type: {type: String, enum: ['SHMEM', 'TCP']},
+            host: String,
+            port: Number,
+            memorySize: Number,
+            tokenDirectoryPath: String,
+            threadCount: Number
+        },
+        ipcEndpointEnabled: Boolean,
+        maxSpaceSize: Number,
+        maximumTaskRangeLength: Number,
+        managementPort: Number,
+        pathModes: [{path: String, mode: {type: String, enum: ['PRIMARY', 'PROXY', 'DUAL_SYNC', 'DUAL_ASYNC']}}],
+        perNodeBatchSize: Number,
+        perNodeParallelBatchCount: Number,
+        prefetchBlocks: Number,
+        sequentialReadsBeforePrefetch: Number,
+        trashPurgeTimeout: Number,
+        secondaryFileSystemEnabled: Boolean,
+        secondaryFileSystem: {
+            userName: String,
+            kind: {type: String, enum: ['Caching', 'Kerberos', 'Custom'], default: 'Caching'},
+            uri: String,
+            cfgPath: String,
+            cfgPaths: [String],
+            userNameMapper: {
+                kind: {type: String, enum: ['Chained', 'Basic', 'Kerberos', 'Custom']},
+                Chained: {
+                    mappers: [{
+                        kind: {type: String, enum: ['Basic', 'Kerberos', 'Custom']},
+                        Basic: {
+                            defaultUserName: String,
+                            useDefaultUserName: Boolean,
+                            mappings: [{
+                                name: String,
+                                value: String
+                            }]
+                        },
+                        Kerberos: {
+                            instance: String,
+                            realm: String
+                        },
+                        Custom: {
+                            className: String,
+                        }
+                    }]
+                },
+                Basic: {
+                    defaultUserName: String,
+                    useDefaultUserName: Boolean,
+                    mappings: [{
+                        name: String,
+                        value: String
+                    }]
+                },
+                Kerberos: {
+                    instance: String,
+                    realm: String
+                },
+                Custom: {
+                    className: String,
+                }
+            },
+            Kerberos: {
+                keyTab: String,
+                keyTabPrincipal: String,
+                reloginInterval: Number
+            },
+            Custom: {
+                className: String
+            }
+        },
+        colocateMetadata: Boolean,
+        relaxedConsistency: Boolean,
+        updateFileLengthOnFlush: Boolean
+    });
+
+    Igfs.index({name: 1, space: 1, clusters: 1}, {unique: true});
+
+
+    // Define Cluster schema.
+    const Cluster = new Schema({
+        space: {type: ObjectId, ref: 'Space', index: true, required: true},
+        name: {type: String},
+        activeOnStart: {type: Boolean, default: true},
+        localHost: String,
+        discovery: {
+            localAddress: String,
+            localPort: Number,
+            localPortRange: Number,
+            addressResolver: String,
+            socketTimeout: Number,
+            ackTimeout: Number,
+            maxAckTimeout: Number,
+            networkTimeout: Number,
+            joinTimeout: Number,
+            threadPriority: Number,
+            heartbeatFrequency: Number,
+            maxMissedHeartbeats: Number,
+            maxMissedClientHeartbeats: Number,
+            topHistorySize: Number,
+            listener: String,
+            dataExchange: String,
+            metricsProvider: String,
+            reconnectCount: Number,
+            statisticsPrintFrequency: Number,
+            ipFinderCleanFrequency: Number,
+            authenticator: String,
+            forceServerMode: Boolean,
+            clientReconnectDisabled: Boolean,
+            connectionRecoveryTimeout: Number,
+            reconnectDelay: Number,
+            soLinger: Number,
+            kind: {
+                type: String,
+                enum: ['Vm', 'Multicast', 'S3', 'Cloud', 'GoogleStorage', 'Jdbc', 'SharedFs', 'ZooKeeper', 'Kubernetes']
+            },
+            Vm: {
+                addresses: [String]
+            },
+            Multicast: {
+                multicastGroup: String,
+                multicastPort: Number,
+                responseWaitTime: Number,
+                addressRequestAttempts: Number,
+                localAddress: String,
+                addresses: [String]
+            },
+            S3: {
+                bucketName: String,
+                bucketEndpoint: String,
+                SSEAlgorithm: String,
+                clientConfiguration: {
+                    protocol: {type: String, enum: ['HTTP', 'HTTPS']},
+                    maxConnections: Number,
+                    userAgentPrefix: String,
+                    userAgentSuffix: String,
+                    localAddress: String,
+                    proxyHost: String,
+                    proxyPort: Number,
+                    proxyUsername: String,
+                    proxyDomain: String,
+                    proxyWorkstation: String,
+                    retryPolicy: {
+                        kind: {
+                            type: String,
+                            enum: ['Default', 'DefaultMaxRetries', 'DynamoDB', 'DynamoDBMaxRetries', 'Custom', 'CustomClass']
+                        },
+                        DefaultMaxRetries: {
+                            maxErrorRetry: Number
+                        },
+                        DynamoDBMaxRetries: {
+                            maxErrorRetry: Number
+                        },
+                        Custom: {
+                            retryCondition: String,
+                            backoffStrategy: String,
+                            maxErrorRetry: Number,
+                            honorMaxErrorRetryInClientConfig: Boolean
+                        },
+                        CustomClass: {
+                            className: String
+                        }
+                    },
+                    maxErrorRetry: Number,
+                    socketTimeout: Number,
+                    connectionTimeout: Number,
+                    requestTimeout: Number,
+                    useReaper: Boolean,
+                    useGzip: Boolean,
+                    signerOverride: String,
+                    preemptiveBasicProxyAuth: Boolean,
+                    connectionTTL: Number,
+                    connectionMaxIdleMillis: Number,
+                    useTcpKeepAlive: Boolean,
+                    dnsResolver: String,
+                    responseMetadataCacheSize: Number,
+                    secureRandom: String,
+                    cacheResponseMetadata: {type: Boolean, default: true},
+                    clientExecutionTimeout: Number,
+                    nonProxyHosts: String,
+                    socketSendBufferSizeHint: Number,
+                    socketReceiveBufferSizeHint: Number,
+                    useExpectContinue: {type: Boolean, default: true},
+                    useThrottleRetries: {type: Boolean, default: true}
+                }
+            },
+            Cloud: {
+                credential: String,
+                credentialPath: String,
+                identity: String,
+                provider: String,
+                regions: [String],
+                zones: [String]
+            },
+            GoogleStorage: {
+                projectName: String,
+                bucketName: String,
+                serviceAccountP12FilePath: String,
+                serviceAccountId: String,
+                addrReqAttempts: String
+            },
+            Jdbc: {
+                initSchema: Boolean,
+                dataSourceBean: String,
+                dialect: {
+                    type: String,
+                    enum: ['Generic', 'Oracle', 'DB2', 'SQLServer', 'MySQL', 'PostgreSQL', 'H2']
+                }
+            },
+            SharedFs: {
+                path: String
+            },
+            ZooKeeper: {
+                curator: String,
+                zkConnectionString: String,
+                retryPolicy: {
+                    kind: {
+                        type: String, enum: ['ExponentialBackoff', 'BoundedExponentialBackoff', 'UntilElapsed',
+                            'NTimes', 'OneTime', 'Forever', 'Custom']
+                    },
+                    ExponentialBackoff: {
+                        baseSleepTimeMs: Number,
+                        maxRetries: Number,
+                        maxSleepMs: Number
+                    },
+                    BoundedExponentialBackoff: {
+                        baseSleepTimeMs: Number,
+                        maxSleepTimeMs: Number,
+                        maxRetries: Number
+                    },
+                    UntilElapsed: {
+                        maxElapsedTimeMs: Number,
+                        sleepMsBetweenRetries: Number
+                    },
+                    NTimes: {
+                        n: Number,
+                        sleepMsBetweenRetries: Number
+                    },
+                    OneTime: {
+                        sleepMsBetweenRetry: Number
+                    },
+                    Forever: {
+                        retryIntervalMs: Number
+                    },
+                    Custom: {
+                        className: String
+                    }
+                },
+                basePath: String,
+                serviceName: String,
+                allowDuplicateRegistrations: Boolean
+            },
+            Kubernetes: {
+                serviceName: String,
+                namespace: String,
+                masterUrl: String,
+                accountToken: String
+            }
+        },
+        atomicConfiguration: {
+            backups: Number,
+            cacheMode: {type: String, enum: ['LOCAL', 'REPLICATED', 'PARTITIONED']},
+            atomicSequenceReserveSize: Number,
+            affinity: {
+                kind: {type: String, enum: ['Default', 'Rendezvous', 'Custom']},
+                Rendezvous: {
+                    affinityBackupFilter: String,
+                    partitions: Number,
+                    excludeNeighbors: Boolean
+                },
+                Custom: {
+                    className: String
+                }
+            },
+            groupName: String
+        },
+        binaryConfiguration: {
+            idMapper: String,
+            nameMapper: String,
+            serializer: String,
+            typeConfigurations: [{
+                typeName: String,
+                idMapper: String,
+                nameMapper: String,
+                serializer: String,
+                enum: Boolean,
+                enumValues: [String]
+            }],
+            compactFooter: Boolean
+        },
+        caches: [{type: ObjectId, ref: 'Cache'}],
+        models: [{type: ObjectId, ref: 'DomainModel'}],
+        clockSyncSamples: Number,
+        clockSyncFrequency: Number,
+        deploymentMode: {type: String, enum: ['PRIVATE', 'ISOLATED', 'SHARED', 'CONTINUOUS']},
+        discoveryStartupDelay: Number,
+        igfsThreadPoolSize: Number,
+        igfss: [{type: ObjectId, ref: 'Igfs'}],
+        includeEventTypes: [String],
+        eventStorage: {
+            kind: {type: String, enum: ['Memory', 'Custom']},
+            Memory: {
+                expireAgeMs: Number,
+                expireCount: Number,
+                filter: String
+            },
+            Custom: {
+                className: String
+            }
+        },
+        managementThreadPoolSize: Number,
+        marshaller: {
+            kind: {type: String, enum: ['OptimizedMarshaller', 'JdkMarshaller']},
+            OptimizedMarshaller: {
+                poolSize: Number,
+                requireSerializable: Boolean
+            }
+        },
+        marshalLocalJobs: Boolean,
+        marshallerCacheKeepAliveTime: Number,
+        marshallerCacheThreadPoolSize: Number,
+        metricsExpireTime: Number,
+        metricsHistorySize: Number,
+        metricsLogFrequency: Number,
+        metricsUpdateFrequency: Number,
+        networkTimeout: Number,
+        networkSendRetryDelay: Number,
+        networkSendRetryCount: Number,
+        communication: {
+            listener: String,
+            localAddress: String,
+            localPort: Number,
+            localPortRange: Number,
+            sharedMemoryPort: Number,
+            directBuffer: Boolean,
+            directSendBuffer: Boolean,
+            idleConnectionTimeout: Number,
+            connectTimeout: Number,
+            maxConnectTimeout: Number,
+            reconnectCount: Number,
+            socketSendBuffer: Number,
+            socketReceiveBuffer: Number,
+            messageQueueLimit: Number,
+            slowClientQueueLimit: Number,
+            tcpNoDelay: Boolean,
+            ackSendThreshold: Number,
+            unacknowledgedMessagesBufferSize: Number,
+            socketWriteTimeout: Number,
+            selectorsCount: Number,
+            addressResolver: String,
+            selectorSpins: Number,
+            connectionsPerNode: Number,
+            usePairedConnections: Boolean,
+            filterReachableAddresses: Boolean
+        },
+        connector: {
+            enabled: Boolean,
+            jettyPath: String,
+            host: String,
+            port: Number,
+            portRange: Number,
+            idleTimeout: Number,
+            idleQueryCursorTimeout: Number,
+            idleQueryCursorCheckFrequency: Number,
+            receiveBufferSize: Number,
+            sendBufferSize: Number,
+            sendQueueLimit: Number,
+            directBuffer: Boolean,
+            noDelay: Boolean,
+            selectorCount: Number,
+            threadPoolSize: Number,
+            messageInterceptor: String,
+            secretKey: String,
+            sslEnabled: Boolean,
+            sslClientAuth: Boolean,
+            sslFactory: String
+        },
+        peerClassLoadingEnabled: Boolean,
+        peerClassLoadingLocalClassPathExclude: [String],
+        peerClassLoadingMissedResourcesCacheSize: Number,
+        peerClassLoadingThreadPoolSize: Number,
+        publicThreadPoolSize: Number,
+        swapSpaceSpi: {
+            kind: {type: String, enum: ['FileSwapSpaceSpi']},
+            FileSwapSpaceSpi: {
+                baseDirectory: String,
+                readStripesNumber: Number,
+                maximumSparsity: Number,
+                maxWriteQueueSize: Number,
+                writeBufferSize: Number
+            }
+        },
+        systemThreadPoolSize: Number,
+        timeServerPortBase: Number,
+        timeServerPortRange: Number,
+        transactionConfiguration: {
+            defaultTxConcurrency: {type: String, enum: ['OPTIMISTIC', 'PESSIMISTIC']},
+            defaultTxIsolation: {type: String, enum: ['READ_COMMITTED', 'REPEATABLE_READ', 'SERIALIZABLE']},
+            defaultTxTimeout: Number,
+            pessimisticTxLogLinger: Number,
+            pessimisticTxLogSize: Number,
+            txSerializableEnabled: Boolean,
+            txManagerFactory: String,
+            useJtaSynchronization: Boolean,
+            txTimeoutOnPartitionMapExchange: Number, // 2.5
+            deadlockTimeout: Number // 2.8
+        },
+        sslEnabled: Boolean,
+        sslContextFactory: {
+            keyAlgorithm: String,
+            keyStoreFilePath: String,
+            keyStoreType: String,
+            protocol: String,
+            trustStoreFilePath: String,
+            trustStoreType: String,
+            trustManagers: [String],
+            cipherSuites: [String],
+            protocols: [String]
+        },
+        rebalanceThreadPoolSize: Number,
+        odbc: {
+            odbcEnabled: Boolean,
+            endpointAddress: String,
+            socketSendBufferSize: Number,
+            socketReceiveBufferSize: Number,
+            maxOpenCursors: Number,
+            threadPoolSize: Number
+        },
+        attributes: [{name: String, value: String}],
+        collision: {
+            kind: {type: String, enum: ['Noop', 'PriorityQueue', 'FifoQueue', 'JobStealing', 'Custom']},
+            PriorityQueue: {
+                parallelJobsNumber: Number,
+                waitingJobsNumber: Number,
+                priorityAttributeKey: String,
+                jobPriorityAttributeKey: String,
+                defaultPriority: Number,
+                starvationIncrement: Number,
+                starvationPreventionEnabled: Boolean
+            },
+            FifoQueue: {
+                parallelJobsNumber: Number,
+                waitingJobsNumber: Number
+            },
+            JobStealing: {
+                activeJobsThreshold: Number,
+                waitJobsThreshold: Number,
+                messageExpireTime: Number,
+                maximumStealingAttempts: Number,
+                stealingEnabled: Boolean,
+                stealingAttributes: [{name: String, value: String}],
+                externalCollisionListener: String
+            },
+            Custom: {
+                class: String
+            }
+        },
+        failoverSpi: [{
+            kind: {type: String, enum: ['JobStealing', 'Never', 'Always', 'Custom']},
+            JobStealing: {
+                maximumFailoverAttempts: Number
+            },
+            Always: {
+                maximumFailoverAttempts: Number
+            },
+            Custom: {
+                class: String
+            }
+        }],
+        logger: {
+            kind: {type: 'String', enum: ['Log4j2', 'Null', 'Java', 'JCL', 'SLF4J', 'Log4j', 'Custom']},
+            Log4j2: {
+                level: {type: String, enum: ['OFF', 'FATAL', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE', 'ALL']},
+                path: String
+            },
+            Log4j: {
+                mode: {type: String, enum: ['Default', 'Path']},
+                level: {type: String, enum: ['OFF', 'FATAL', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE', 'ALL']},
+                path: String
+            },
+            Custom: {
+                class: String
+            }
+        },
+        cacheKeyConfiguration: [{
+            typeName: String,
+            affinityKeyFieldName: String
+        }],
+        checkpointSpi: [{
+            kind: {type: String, enum: ['FS', 'Cache', 'S3', 'JDBC', 'Custom']},
+            FS: {
+                directoryPaths: [String],
+                checkpointListener: String
+            },
+            Cache: {
+                cache: {type: ObjectId, ref: 'Cache'},
+                checkpointListener: String
+            },
+            S3: {
+                awsCredentials: {
+                    kind: {type: String, enum: ['Basic', 'Properties', 'Anonymous', 'BasicSession', 'Custom']},
+                    Properties: {
+                        path: String
+                    },
+                    Custom: {
+                        className: String
+                    }
+                },
+                bucketNameSuffix: String,
+                bucketEndpoint: String,
+                SSEAlgorithm: String,
+                clientConfiguration: {
+                    protocol: {type: String, enum: ['HTTP', 'HTTPS']},
+                    maxConnections: Number,
+                    userAgentPrefix: String,
+                    userAgentSuffix: String,
+                    localAddress: String,
+                    proxyHost: String,
+                    proxyPort: Number,
+                    proxyUsername: String,
+                    proxyDomain: String,
+                    proxyWorkstation: String,
+                    retryPolicy: {
+                        kind: {
+                            type: String,
+                            enum: ['Default', 'DefaultMaxRetries', 'DynamoDB', 'DynamoDBMaxRetries', 'Custom']
+                        },
+                        DefaultMaxRetries: {
+                            maxErrorRetry: Number
+                        },
+                        DynamoDBMaxRetries: {
+                            maxErrorRetry: Number
+                        },
+                        Custom: {
+                            retryCondition: String,
+                            backoffStrategy: String,
+                            maxErrorRetry: Number,
+                            honorMaxErrorRetryInClientConfig: Boolean
+                        }
+                    },
+                    maxErrorRetry: Number,
+                    socketTimeout: Number,
+                    connectionTimeout: Number,
+                    requestTimeout: Number,
+                    useReaper: Boolean,
+                    useGzip: Boolean,
+                    signerOverride: String,
+                    preemptiveBasicProxyAuth: Boolean,
+                    connectionTTL: Number,
+                    connectionMaxIdleMillis: Number,
+                    useTcpKeepAlive: Boolean,
+                    dnsResolver: String,
+                    responseMetadataCacheSize: Number,
+                    secureRandom: String,
+                    cacheResponseMetadata: {type: Boolean, default: true},
+                    clientExecutionTimeout: Number,
+                    nonProxyHosts: String,
+                    socketSendBufferSizeHint: Number,
+                    socketReceiveBufferSizeHint: Number,
+                    useExpectContinue: {type: Boolean, default: true},
+                    useThrottleRetries: {type: Boolean, default: true}
+                },
+                checkpointListener: String
+            },
+            JDBC: {
+                dataSourceBean: String,
+                dialect: {
+                    type: String,
+                    enum: ['Generic', 'Oracle', 'DB2', 'SQLServer', 'MySQL', 'PostgreSQL', 'H2']
+                },
+                user: String,
+                checkpointTableName: String,
+                keyFieldName: String,
+                keyFieldType: String,
+                valueFieldName: String,
+                valueFieldType: String,
+                expireDateFieldName: String,
+                expireDateFieldType: String,
+                numberOfRetries: Number,
+                checkpointListener: String
+            },
+            Custom: {
+                className: String
+            }
+        }],
+        clientConnectorConfiguration: {
+            enabled: Boolean,
+            host: String,
+            port: Number,
+            portRange: Number,
+            socketSendBufferSize: Number,
+            socketReceiveBufferSize: Number,
+            tcpNoDelay: {type: Boolean, default: true},
+            maxOpenCursorsPerConnection: Number,
+            threadPoolSize: Number,
+            idleTimeout: Number,
+            jdbcEnabled: {type: Boolean, default: true},
+            odbcEnabled: {type: Boolean, default: true},
+            thinClientEnabled: {type: Boolean, default: true},
+            sslEnabled: Boolean,
+            useIgniteSslContextFactory: {type: Boolean, default: true},
+            sslClientAuth: Boolean,
+            sslContextFactory: String
+        },
+        loadBalancingSpi: [{
+            kind: {type: String, enum: ['RoundRobin', 'Adaptive', 'WeightedRandom', 'Custom']},
+            RoundRobin: {
+                perTask: Boolean
+            },
+            Adaptive: {
+                loadProbe: {
+                    kind: {type: String, enum: ['Job', 'CPU', 'ProcessingTime', 'Custom']},
+                    Job: {
+                        useAverage: Boolean
+                    },
+                    CPU: {
+                        useAverage: Boolean,
+                        useProcessors: Boolean,
+                        processorCoefficient: Number
+                    },
+                    ProcessingTime: {
+                        useAverage: Boolean
+                    },
+                    Custom: {
+                        className: String
+                    }
+                }
+            },
+            WeightedRandom: {
+                nodeWeight: Number,
+                useWeights: Boolean
+            },
+            Custom: {
+                className: String
+            }
+        }],
+        deploymentSpi: {
+            kind: {type: String, enum: ['URI', 'Local', 'Custom']},
+            URI: {
+                uriList: [String],
+                temporaryDirectoryPath: String,
+                scanners: [String],
+                listener: String,
+                checkMd5: Boolean,
+                encodeUri: Boolean
+            },
+            Local: {
+                listener: String
+            },
+            Custom: {
+                className: String
+            }
+        },
+        warmupClosure: String,
+        hadoopConfiguration: {
+            mapReducePlanner: {
+                kind: {type: String, enum: ['Weighted', 'Custom']},
+                Weighted: {
+                    localMapperWeight: Number,
+                    remoteMapperWeight: Number,
+                    localReducerWeight: Number,
+                    remoteReducerWeight: Number,
+                    preferLocalReducerThresholdWeight: Number
+                },
+                Custom: {
+                    className: String
+                }
+            },
+            finishedJobInfoTtl: Number,
+            maxParallelTasks: Number,
+            maxTaskQueueSize: Number,
+            nativeLibraryNames: [String]
+        },
+        serviceConfigurations: [{
+            name: String,
+            service: String,
+            maxPerNodeCount: Number,
+            totalCount: Number,
+            nodeFilter: {
+                kind: {type: String, enum: ['Default', 'Exclude', 'IGFS', 'OnNodes', 'Custom']},
+                Exclude: {
+                    nodeId: String
+                },
+                IGFS: {
+                    igfs: {type: ObjectId, ref: 'Igfs'}
+                },
+                Custom: {
+                    className: String
+                }
+            },
+            cache: {type: ObjectId, ref: 'Cache'},
+            affinityKey: String
+        }],
+        cacheSanityCheckEnabled: {type: Boolean, default: true},
+        classLoader: String,
+        consistentId: String,
+        failureDetectionTimeout: Number,
+        clientFailureDetectionTimeout: Number,
+        systemWorkerBlockedTimeout: Number,
+        workDirectory: String,
+        igniteHome: String,
+        lateAffinityAssignment: Boolean,
+        utilityCacheKeepAliveTime: Number,
+        asyncCallbackPoolSize: Number,
+        dataStreamerThreadPoolSize: Number,
+        queryThreadPoolSize: Number,
+        stripedPoolSize: Number,
+        serviceThreadPoolSize: Number,
+        utilityCacheThreadPoolSize: Number,
+        executorConfiguration: [{
+            name: String,
+            size: Number
+        }],
+        dataStorageConfiguration: {
+            systemRegionInitialSize: Number,
+            systemRegionMaxSize: Number,
+            pageSize: Number,
+            concurrencyLevel: Number,
+            defaultDataRegionConfiguration: {
+                name: String,
+                initialSize: Number,
+                maxSize: Number,
+                swapPath: String,
+                pageEvictionMode: {type: String, enum: ['DISABLED', 'RANDOM_LRU', 'RANDOM_2_LRU']},
+                evictionThreshold: Number,
+                emptyPagesPoolSize: Number,
+                metricsEnabled: Boolean,
+                metricsSubIntervalCount: Number,
+                metricsRateTimeInterval: Number,
+                persistenceEnabled: Boolean,
+                checkpointPageBufferSize: Number
+            },
+            dataRegionConfigurations: [{
+                name: String,
+                initialSize: Number,
+                maxSize: Number,
+                swapPath: String,
+                pageEvictionMode: {type: String, enum: ['DISABLED', 'RANDOM_LRU', 'RANDOM_2_LRU']},
+                evictionThreshold: Number,
+                emptyPagesPoolSize: Number,
+                metricsEnabled: Boolean,
+                metricsSubIntervalCount: Number,
+                metricsRateTimeInterval: Number,
+                persistenceEnabled: Boolean,
+                checkpointPageBufferSize: Number
+            }],
+            storagePath: String,
+            metricsEnabled: Boolean,
+            alwaysWriteFullPages: Boolean,
+            checkpointFrequency: Number,
+            checkpointThreads: Number,
+            checkpointWriteOrder: {type: String, enum: ['RANDOM', 'SEQUENTIAL']},
+            walPath: String,
+            walArchivePath: String,
+            walMode: {type: String, enum: ['DEFAULT', 'LOG_ONLY', 'BACKGROUND', 'NONE']},
+            walSegments: Number,
+            walSegmentSize: Number,
+            walHistorySize: Number,
+            walFlushFrequency: Number,
+            walFsyncDelayNanos: Number,
+            walRecordIteratorBufferSize: Number,
+            lockWaitTime: Number,
+            walBufferSize: Number,
+            walThreadLocalBufferSize: Number,
+            metricsSubIntervalCount: Number,
+            metricsRateTimeInterval: Number,
+            fileIOFactory: {type: String, enum: ['RANDOM', 'ASYNC']},
+            walAutoArchiveAfterInactivity: Number,
+            writeThrottlingEnabled: Boolean,
+            walCompactionEnabled: Boolean,
+            checkpointReadLockTimeout: Number,
+            maxWalArchiveSize: Number,
+            walCompactionLevel: Number
+        },
+        memoryConfiguration: {
+            systemCacheInitialSize: Number,
+            systemCacheMaxSize: Number,
+            pageSize: Number,
+            concurrencyLevel: Number,
+            defaultMemoryPolicyName: String,
+            defaultMemoryPolicySize: Number,
+            memoryPolicies: [{
+                name: String,
+                initialSize: Number,
+                maxSize: Number,
+                swapFilePath: String,
+                pageEvictionMode: {type: String, enum: ['DISABLED', 'RANDOM_LRU', 'RANDOM_2_LRU']},
+                evictionThreshold: Number,
+                emptyPagesPoolSize: Number,
+                metricsEnabled: Boolean,
+                subIntervals: Number,
+                rateTimeInterval: Number
+            }]
+        },
+        longQueryWarningTimeout: Number,
+        sqlConnectorConfiguration: {
+            enabled: Boolean,
+            host: String,
+            port: Number,
+            portRange: Number,
+            socketSendBufferSize: Number,
+            socketReceiveBufferSize: Number,
+            tcpNoDelay: {type: Boolean, default: true},
+            maxOpenCursorsPerConnection: Number,
+            threadPoolSize: Number
+        },
+        persistenceStoreConfiguration: {
+            enabled: Boolean,
+            persistentStorePath: String,
+            metricsEnabled: Boolean,
+            alwaysWriteFullPages: Boolean,
+            checkpointingFrequency: Number,
+            checkpointingPageBufferSize: Number,
+            checkpointingThreads: Number,
+            walStorePath: String,
+            walArchivePath: String,
+            walMode: {type: String, enum: ['DEFAULT', 'LOG_ONLY', 'BACKGROUND', 'NONE']},
+            walSegments: Number,
+            walSegmentSize: Number,
+            walHistorySize: Number,
+            walFlushFrequency: Number,
+            walFsyncDelayNanos: Number,
+            walRecordIteratorBufferSize: Number,
+            lockWaitTime: Number,
+            rateTimeInterval: Number,
+            tlbSize: Number,
+            subIntervals: Number,
+            walAutoArchiveAfterInactivity: Number
+        },
+        encryptionSpi: {
+            kind: {type: String, enum: ['Noop', 'Keystore', 'Custom']},
+            Keystore: {
+                keySize: Number,
+                masterKeyName: String,
+                keyStorePath: String
+            },
+            Custom: {
+                className: String
+            }
+        },
+        failureHandler: {
+            kind: {type: String, enum: ['RestartProcess', 'StopNodeOnHalt', 'StopNode', 'Noop', 'Custom']},
+            ignoredFailureTypes: [{type: String, enum: ['SEGMENTATION', 'SYSTEM_WORKER_TERMINATION',
+                    'SYSTEM_WORKER_BLOCKED', 'CRITICAL_ERROR', 'SYSTEM_CRITICAL_OPERATION_TIMEOUT']}],
+            StopNodeOnHalt: {
+                tryStop: Boolean,
+                timeout: Number
+            },
+            Custom: {
+                className: String
+            }
+        },
+        localEventListeners: [{
+            className: String,
+            eventTypes: [String]
+        }],
+        mvccVacuumThreadCount: Number,
+        mvccVacuumFrequency: Number,
+        authenticationEnabled: Boolean,
+        sqlQueryHistorySize: Number,
+        lifecycleBeans: [String],
+        addressResolver: String,
+        mBeanServer: String,
+        networkCompressionLevel: Number,
+        includeProperties: [String],
+        cacheStoreSessionListenerFactories: [String],
+        autoActivationEnabled: {type: Boolean, default: true},
+        sqlSchemas: [String],
+        communicationFailureResolver: String
+    });
+
+    Cluster.index({name: 1, space: 1}, {unique: true});
+
+    // Define Notebook schema.
+    const Notebook = new Schema({
+        space: {type: ObjectId, ref: 'Space', index: true, required: true},
+        name: String,
+        expandedParagraphs: [Number],
+        paragraphs: [{
+            name: String,
+            query: String,
+            editor: Boolean,
+            result: {type: String, enum: ['none', 'table', 'bar', 'pie', 'line', 'area']},
+            pageSize: Number,
+            timeLineSpan: String,
+            maxPages: Number,
+            hideSystemColumns: Boolean,
+            cacheName: String,
+            useAsDefaultSchema: Boolean,
+            chartsOptions: {barChart: {stacked: Boolean}, areaChart: {style: String}},
+            rate: {
+                value: Number,
+                unit: Number
+            },
+            qryType: String,
+            nonCollocatedJoins: {type: Boolean, default: false},
+            enforceJoinOrder: {type: Boolean, default: false},
+            lazy: {type: Boolean, default: false},
+            collocated: Boolean
+        }]
+    });
+
+    Notebook.index({name: 1, space: 1}, {unique: true});
+
+    // Define Activities schema.
+    const Activities = new Schema({
+        owner: {type: ObjectId, ref: 'Account'},
+        date: Date,
+        group: String,
+        action: String,
+        amount: {type: Number, default: 0}
+    });
+
+    Activities.index({owner: 1, group: 1, action: 1, date: 1}, {unique: true});
+
+    // Define Notifications schema.
+    const Notifications = new Schema({
+        owner: {type: ObjectId, ref: 'Account'},
+        date: Date,
+        message: String,
+        isShown: Boolean
+    });
+
+    return {
+        Space,
+        Account,
+        DomainModel,
+        Cache,
+        Igfs,
+        Cluster,
+        Notebook,
+        Activities,
+        Notifications
+    };
+};
diff --git a/modules/backend/app/settings.js b/modules/backend/app/settings.js
new file mode 100644
index 0000000..0079788
--- /dev/null
+++ b/modules/backend/app/settings.js
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const fs = require('fs');
+const _ = require('lodash');
+
+// Fire me up!
+
+/**
+ * Module with server-side configuration.
+ */
+module.exports = {
+    implements: 'settings',
+    inject: ['nconf'],
+    factory(nconf) {
+        /**
+         * Normalize a port into a number, string, or false.
+         */
+        const _normalizePort = function(val) {
+            const port = parseInt(val, 10);
+
+            // named pipe
+            if (isNaN(port))
+                return val;
+
+            // port number
+            if (port >= 0)
+                return port;
+
+            return false;
+        };
+
+        const mail = nconf.get('mail') || {};
+
+        const packaged = __dirname.startsWith('/snapshot/') || __dirname.startsWith('C:\\snapshot\\');
+
+        const dfltAgentDists = packaged ? 'libs/agent_dists' : 'agent_dists';
+        const dfltHost = packaged ? '0.0.0.0' : '127.0.0.1';
+        const dfltPort = packaged ? 80 : 3000;
+
+        // We need this function because nconf() can return String or Boolean.
+        // And in JS we cannot compare String with Boolean.
+        const _isTrue = (confParam) => {
+            const v = nconf.get(confParam);
+
+            return v === 'true' || v === true;
+        };
+
+        let activationEnabled = _isTrue('activation:enabled');
+
+        if (activationEnabled && _.isEmpty(mail)) {
+            activationEnabled = false;
+
+            console.warn('Mail server settings are required for account confirmation!');
+        }
+
+        const settings = {
+            agent: {
+                dists: nconf.get('agent:dists') || dfltAgentDists
+            },
+            packaged,
+            server: {
+                host: nconf.get('server:host') || dfltHost,
+                port: _normalizePort(nconf.get('server:port') || dfltPort),
+                disableSignup: _isTrue('server:disable:signup')
+            },
+            mail,
+            activation: {
+                enabled: activationEnabled,
+                timeout: nconf.get('activation:timeout') || 1800000,
+                sendTimeout: nconf.get('activation:sendTimeout') || 180000
+            },
+            mongoUrl: nconf.get('mongodb:url') || 'mongodb://127.0.0.1/console',
+            cookieTTL: 3600000 * 24 * 30,
+            sessionSecret: nconf.get('server:sessionSecret') || 'keyboard cat',
+            tokenLength: 20
+        };
+
+        // Configure SSL options.
+        if (_isTrue('server:ssl')) {
+            const sslOptions = {
+                enable301Redirects: true,
+                trustXFPHeader: true,
+                isServer: true
+            };
+
+            const setSslOption = (name, fromFile = false) => {
+                const v = nconf.get(`server:${name}`);
+
+                const hasOption = !!v;
+
+                if (hasOption)
+                    sslOptions[name] = fromFile ? fs.readFileSync(v) : v;
+
+                return hasOption;
+            };
+
+            const setSslOptionBoolean = (name) => {
+                const v = nconf.get(`server:${name}`);
+
+                if (v)
+                    sslOptions[name] = v === 'true' || v === true;
+            };
+
+            setSslOption('key', true);
+            setSslOption('cert', true);
+            setSslOption('ca', true);
+            setSslOption('passphrase');
+            setSslOption('ciphers');
+            setSslOption('secureProtocol');
+            setSslOption('clientCertEngine');
+            setSslOption('pfx', true);
+            setSslOption('crl');
+            setSslOption('dhparam');
+            setSslOption('ecdhCurve');
+            setSslOption('maxVersion');
+            setSslOption('minVersion');
+            setSslOption('secureOptions');
+            setSslOption('sessionIdContext');
+
+            setSslOptionBoolean('honorCipherOrder');
+            setSslOptionBoolean('requestCert');
+            setSslOptionBoolean('rejectUnauthorized');
+
+            // Special care for case, when user set password for something like "123456".
+            if (sslOptions.passphrase)
+                sslOptions.passphrase = sslOptions.passphrase.toString();
+
+            settings.server.SSLOptions = sslOptions;
+        }
+
+        return settings;
+    }
+};
diff --git a/modules/backend/config/settings.json.sample b/modules/backend/config/settings.json.sample
new file mode 100644
index 0000000..02bc327
--- /dev/null
+++ b/modules/backend/config/settings.json.sample
@@ -0,0 +1,36 @@
+{
+  "server": {
+    "port": 3000,
+    "sessionSecret": "CHANGE ME",
+    "ssl": false,
+    "key": "path to file with server.key",
+    "cert": "path to file with server.crt",
+    "ca": "path to file with ca.crt",
+    "passphrase": "password",
+    "ciphers": "ECDHE-RSA-AES128-GCM-SHA256",
+    "secureProtocol": "TLSv1_2_method",
+    "requestCert": false,
+    "rejectUnauthorized": false,
+    "disable": {
+      "signup": false
+    }
+  },
+  "mongodb": {
+    "url": "mongodb://localhost/console"
+  },
+  "activation": {
+      "enabled": false,
+      "timeout": 1800000,
+      "sendTimeout": 180000
+  },
+  "mail": {
+    "service": "gmail",
+    "from": "Some Company Web Console <some_username@some_company.com>",
+    "greeting": "Some Company Web Console",
+    "sign": "Kind regards,<br>Some Company Team",
+    "auth": {
+      "user": "some_username@some_company.com",
+      "pass": "CHANGE ME"
+    }
+  }
+}
diff --git a/modules/backend/errors/AppErrorException.js b/modules/backend/errors/AppErrorException.js
new file mode 100644
index 0000000..19a9b0d
--- /dev/null
+++ b/modules/backend/errors/AppErrorException.js
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+class AppErrorException extends Error {
+    constructor(message) {
+        super(message);
+
+        this.name = this.constructor.name;
+        this.code = 400;
+
+        if (typeof Error.captureStackTrace === 'function')
+            Error.captureStackTrace(this, this.constructor);
+        else
+            this.stack = (new Error(message)).stack;
+    }
+}
+
+module.exports = AppErrorException;
diff --git a/modules/backend/errors/AuthFailedException.js b/modules/backend/errors/AuthFailedException.js
new file mode 100644
index 0000000..9cab6ac
--- /dev/null
+++ b/modules/backend/errors/AuthFailedException.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const AppErrorException = require('./AppErrorException');
+
+class AuthFailedException extends AppErrorException {
+    constructor(message) {
+        super(message);
+
+        this.code = 401;
+    }
+}
+
+module.exports = AuthFailedException;
diff --git a/modules/backend/errors/DuplicateKeyException.js b/modules/backend/errors/DuplicateKeyException.js
new file mode 100644
index 0000000..536d53d
--- /dev/null
+++ b/modules/backend/errors/DuplicateKeyException.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const AppErrorException = require('./AppErrorException');
+
+class DuplicateKeyException extends AppErrorException {
+    constructor(message) {
+        super(message);
+    }
+}
+
+module.exports = DuplicateKeyException;
diff --git a/modules/backend/errors/IllegalAccessError.js b/modules/backend/errors/IllegalAccessError.js
new file mode 100644
index 0000000..7de9bb1
--- /dev/null
+++ b/modules/backend/errors/IllegalAccessError.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const AppErrorException = require('./AppErrorException');
+
+class IllegalAccessError extends AppErrorException {
+    constructor(message) {
+        super(message);
+
+        this.code = 403;
+    }
+}
+
+module.exports = IllegalAccessError;
diff --git a/modules/backend/errors/IllegalArgumentException.js b/modules/backend/errors/IllegalArgumentException.js
new file mode 100644
index 0000000..aeb4187
--- /dev/null
+++ b/modules/backend/errors/IllegalArgumentException.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const AppErrorException = require('./AppErrorException');
+
+class IllegalArgumentException extends AppErrorException {
+    constructor(message) {
+        super(message);
+    }
+}
+
+module.exports = IllegalArgumentException;
diff --git a/modules/backend/errors/MissingConfirmRegistrationException.js b/modules/backend/errors/MissingConfirmRegistrationException.js
new file mode 100644
index 0000000..a094a67
--- /dev/null
+++ b/modules/backend/errors/MissingConfirmRegistrationException.js
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const IllegalAccessError = require('./IllegalAccessError');
+
+class MissingConfirmRegistrationException extends IllegalAccessError {
+    constructor(email) {
+        super('User account email not activated');
+
+        this.data = {
+            errorCode: 10104,
+            message: this.message,
+            email
+        };
+    }
+}
+
+module.exports = MissingConfirmRegistrationException;
diff --git a/modules/backend/errors/MissingResourceException.js b/modules/backend/errors/MissingResourceException.js
new file mode 100644
index 0000000..aeac70e
--- /dev/null
+++ b/modules/backend/errors/MissingResourceException.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const AppErrorException = require('./AppErrorException');
+
+class MissingResourceException extends AppErrorException {
+    constructor(message) {
+        super(message);
+
+        this.code = 404;
+    }
+}
+
+module.exports = MissingResourceException;
diff --git a/modules/backend/errors/ServerErrorException.js b/modules/backend/errors/ServerErrorException.js
new file mode 100644
index 0000000..c2edb7f
--- /dev/null
+++ b/modules/backend/errors/ServerErrorException.js
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+class ServerErrorException extends Error {
+    constructor(message) {
+        super(message);
+
+        this.name = this.constructor.name;
+        this.code = 500;
+        this.message = message;
+
+        if (typeof Error.captureStackTrace === 'function')
+            Error.captureStackTrace(this, this.constructor);
+        else
+            this.stack = (new Error(message)).stack;
+    }
+}
+
+module.exports = ServerErrorException;
diff --git a/modules/backend/errors/index.js b/modules/backend/errors/index.js
new file mode 100644
index 0000000..cb8f043
--- /dev/null
+++ b/modules/backend/errors/index.js
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+const AppErrorException = require('./AppErrorException');
+const IllegalArgumentException = require('./IllegalArgumentException');
+const IllegalAccessError = require('./IllegalAccessError');
+const DuplicateKeyException = require('./DuplicateKeyException');
+const ServerErrorException = require('./ServerErrorException');
+const MissingConfirmRegistrationException = require('./MissingConfirmRegistrationException');
+const MissingResourceException = require('./MissingResourceException');
+const AuthFailedException = require('./AuthFailedException');
+
+module.exports = {
+    implements: 'errors',
+    factory: () => ({
+        AppErrorException,
+        IllegalAccessError,
+        IllegalArgumentException,
+        DuplicateKeyException,
+        ServerErrorException,
+        MissingConfirmRegistrationException,
+        MissingResourceException,
+        AuthFailedException
+    })
+};
diff --git a/modules/backend/index.js b/modules/backend/index.js
new file mode 100755
index 0000000..26584bc
--- /dev/null
+++ b/modules/backend/index.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const path = require('path');
+
+const appPath = require('app-module-path');
+appPath.addPath(__dirname);
+appPath.addPath(path.join(__dirname, 'node_modules'));
+
+const { migrate, init } = require('./launch-tools');
+
+const injector = require('./injector');
+
+injector.log.info = () => {};
+injector.log.debug = () => {};
+
+injector('mongo')
+    .then((mongo) => migrate(mongo.connection, 'Ignite', path.join(__dirname, 'migrations')))
+    .then(() => Promise.all([injector('settings'), injector('api-server'), injector('agents-handler'), injector('browsers-handler')]))
+    .then(init)
+    .catch((err) => {
+        console.error(err);
+
+        process.exit(1);
+    });
diff --git a/modules/backend/injector.js b/modules/backend/injector.js
new file mode 100644
index 0000000..c30609a
--- /dev/null
+++ b/modules/backend/injector.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const fireUp = require('fire-up');
+
+module.exports = fireUp.newInjector({
+    basePath: __dirname,
+    modules: [
+        './app/**/*.js',
+        './errors/**/*.js',
+        './middlewares/**/*.js',
+        './routes/**/*.js',
+        './services/**/*.js'
+    ]
+});
diff --git a/modules/backend/launch-tools.js b/modules/backend/launch-tools.js
new file mode 100644
index 0000000..7b44aca
--- /dev/null
+++ b/modules/backend/launch-tools.js
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const _ = require('lodash');
+const http = require('http');
+const https = require('https');
+const MigrateMongoose = require('migrate-mongoose-typescript');
+
+/**
+ * Event listener for HTTP server "error" event.
+ */
+const _onError = (addr, error) => {
+    if (error.syscall !== 'listen')
+        throw error;
+
+    // Handle specific listen errors with friendly messages.
+    switch (error.code) {
+        case 'EACCES':
+            console.error(`Requires elevated privileges for bind to ${addr}`);
+            process.exit(1);
+
+            break;
+        case 'EADDRINUSE':
+            console.error(`${addr} is already in use`);
+            process.exit(1);
+
+            break;
+        default:
+            throw error;
+    }
+};
+
+/**
+ * @param settings
+ * @param {ApiServer} apiSrv
+ * @param {AgentsHandler} agentsHnd
+ * @param {BrowsersHandler} browsersHnd
+ */
+const init = ([settings, apiSrv, agentsHnd, browsersHnd]) => {
+    // Start rest server.
+    const sslOptions = settings.server.SSLOptions;
+
+    console.log(`Starting ${sslOptions ? 'HTTPS' : 'HTTP'} server`);
+
+    const srv = sslOptions ? https.createServer(sslOptions) : http.createServer();
+
+    srv.listen(settings.server.port, settings.server.host);
+
+    const addr = `${settings.server.host}:${settings.server.port}`;
+
+    srv.on('error', _onError.bind(null, addr));
+    srv.on('listening', () => console.log(`Start listening on ${addr}`));
+
+    apiSrv.attach(srv);
+
+    agentsHnd.attach(srv, browsersHnd);
+    browsersHnd.attach(srv, agentsHnd);
+
+    // Used for automated test.
+    if (process.send)
+        process.send('running');
+};
+
+/**
+ * Run mongo model migration.
+ *
+ * @param connection Mongo connection.
+ * @param group Migrations group.
+ * @param migrationsPath Migrations path.
+ * @param collectionName Name of collection where migrations write info about applied scripts.
+ */
+const migrate = (connection, group, migrationsPath, collectionName) => {
+    const migrator = new MigrateMongoose({
+        migrationsPath,
+        connection,
+        collectionName,
+        autosync: true
+    });
+
+    console.log(`Running ${group} migrations...`);
+
+    return migrator.run('up')
+        .then(() => console.log(`All ${group} migrations finished successfully.`))
+        .catch((err) => {
+            const msg = _.get(err, 'message');
+
+            if (_.startsWith(msg, 'There are no migrations to run') || _.startsWith(msg, 'There are no pending migrations.')) {
+                console.log(`There are no ${group} migrations to run.`);
+
+                return;
+            }
+
+            throw err;
+        });
+};
+
+module.exports = { migrate, init };
diff --git a/modules/backend/middlewares/api.js b/modules/backend/middlewares/api.js
new file mode 100644
index 0000000..d4d832b
--- /dev/null
+++ b/modules/backend/middlewares/api.js
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+const _ = require('lodash');
+
+module.exports = {
+    implements: 'middlewares:api'
+};
+
+module.exports.factory = () => {
+    return (req, res, next) => {
+        // Set headers to avoid API caching in browser (esp. IE)
+        res.header('Cache-Control', 'must-revalidate');
+        res.header('Expires', '-1');
+        res.header('Last-Modified', new Date().toUTCString());
+
+        res.api = {
+            error(err) {
+                if (_.includes(['MongoError', 'MongooseError'], err.name))
+                    return res.status(500).send(err.message);
+
+                if (_.isObject(err.data))
+                    return res.status(err.httpCode || err.code || 500).json(err.data);
+
+                res.status(err.httpCode || err.code || 500).send(err.message);
+            },
+
+            ok(data) {
+                if (_.isNil(data))
+                    return res.sendStatus(404);
+
+                res.status(200).json(data);
+            },
+
+            done() {
+                res.sendStatus(200);
+            }
+        };
+
+        next();
+    };
+};
diff --git a/modules/backend/middlewares/demo.js b/modules/backend/middlewares/demo.js
new file mode 100644
index 0000000..537ede1
--- /dev/null
+++ b/modules/backend/middlewares/demo.js
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'middlewares:demo',
+    factory: () => {
+        return (req, res, next) => {
+            req.demo = () => req.header('IgniteDemoMode');
+
+            next();
+        };
+    }
+};
diff --git a/modules/backend/middlewares/host.js b/modules/backend/middlewares/host.js
new file mode 100644
index 0000000..4c21da2
--- /dev/null
+++ b/modules/backend/middlewares/host.js
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'middlewares:host',
+    factory: () => {
+        return (req, res, next) => {
+            req.origin = function() {
+                if (req.headers.origin)
+                    return req.headers.origin;
+
+                const proto = req.headers['x-forwarded-proto'] || req.protocol;
+
+                const host = req.headers['x-forwarded-host'] || req.get('host');
+
+                return `${proto}://${host}`;
+            };
+
+            next();
+        };
+    }
+};
diff --git a/modules/backend/middlewares/user.js b/modules/backend/middlewares/user.js
new file mode 100644
index 0000000..8923211
--- /dev/null
+++ b/modules/backend/middlewares/user.js
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'middlewares:user',
+    factory: () => {
+        return (req, res, next) => {
+            req.currentUserId = function() {
+                if (req.session.viewedUser && req.user.admin)
+                    return req.session.viewedUser._id;
+
+                return req.user._id;
+            };
+
+            next();
+        };
+    }
+};
diff --git a/modules/backend/migrations/1502249492000-invalidate_rename.js b/modules/backend/migrations/1502249492000-invalidate_rename.js
new file mode 100644
index 0000000..50b1438
--- /dev/null
+++ b/modules/backend/migrations/1502249492000-invalidate_rename.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+exports.up = function up(done) {
+    this('Cache').updateMany({}, { $rename: {invalidate: 'isInvalidate'}})
+        .then(() => done())
+        .catch(done);
+};
+
+exports.down = function down(done) {
+    this('Cache').updateMany({}, { $rename: {isInvalidate: 'invalidate'}})
+        .then(() => done())
+        .catch(done);
+};
diff --git a/modules/backend/migrations/1502432624000-cache-index.js b/modules/backend/migrations/1502432624000-cache-index.js
new file mode 100644
index 0000000..147e2ad
--- /dev/null
+++ b/modules/backend/migrations/1502432624000-cache-index.js
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const recreateIndex = require('./migration-utils').recreateIndex;
+
+exports.up = function up(done) {
+    recreateIndex(done, this('Cache').collection,
+        'name_1_space_1',
+        {name: 1, space: 1},
+        {name: 1, space: 1, clusters: 1});
+};
+
+exports.down = function down(done) {
+    recreateIndex(done, this('Cache').collection,
+        'name_1_space_1_clusters_1',
+        {name: 1, space: 1, clusters: 1},
+        {name: 1, space: 1});
+};
diff --git a/modules/backend/migrations/1504672035000-igfs-index.js b/modules/backend/migrations/1504672035000-igfs-index.js
new file mode 100644
index 0000000..e802ca9
--- /dev/null
+++ b/modules/backend/migrations/1504672035000-igfs-index.js
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const recreateIndex = require('./migration-utils').recreateIndex;
+
+exports.up = function up(done) {
+    recreateIndex(done, this('Igfs').collection,
+        'name_1_space_1',
+        {name: 1, space: 1},
+        {name: 1, space: 1, clusters: 1});
+};
+
+exports.down = function down(done) {
+    recreateIndex(done, this('Igfs').collection,
+        'name_1_space_1_clusters_1',
+        {name: 1, space: 1, clusters: 1},
+        {name: 1, space: 1});
+};
diff --git a/modules/backend/migrations/1505114649000-models-index.js b/modules/backend/migrations/1505114649000-models-index.js
new file mode 100644
index 0000000..c007b01
--- /dev/null
+++ b/modules/backend/migrations/1505114649000-models-index.js
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const recreateIndex = require('./migration-utils').recreateIndex;
+
+exports.up = function up(done) {
+    recreateIndex(done, this('DomainModel').collection,
+        'valueType_1_space_1',
+        {valueType: 1, space: 1},
+        {valueType: 1, space: 1, clusters: 1});
+};
+
+exports.down = function down(done) {
+    recreateIndex(done, this('DomainModel').collection,
+        'valueType_1_space_1_clusters_1',
+        {valueType: 1, space: 1, clusters: 1},
+        {valueType: 1, space: 1});
+};
diff --git a/modules/backend/migrations/1508395969410-init-registered-date.js b/modules/backend/migrations/1508395969410-init-registered-date.js
new file mode 100644
index 0000000..227d743
--- /dev/null
+++ b/modules/backend/migrations/1508395969410-init-registered-date.js
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const _ = require('lodash');
+
+exports.up = function up(done) {
+    const accountsModel = this('Account');
+
+    accountsModel.find({}).lean().exec()
+        .then((accounts) => _.reduce(accounts, (start, account) => start
+            .then(() => accountsModel.updateOne({_id: account._id}, {$set: {registered: account.lastLogin}}).exec()), Promise.resolve()))
+        .then(() => done())
+        .catch(done);
+};
+
+exports.down = function down(done) {
+    this('Account').updateMany({}, {$unset: {registered: 1}}).exec()
+        .then(() => done())
+        .catch(done);
+};
diff --git a/modules/backend/migrations/1516948939797-migrate-configs.js b/modules/backend/migrations/1516948939797-migrate-configs.js
new file mode 100644
index 0000000..c5bd619
--- /dev/null
+++ b/modules/backend/migrations/1516948939797-migrate-configs.js
@@ -0,0 +1,399 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const _ = require('lodash');
+
+const log = require('./migration-utils').log;
+const error = require('./migration-utils').error;
+
+const getClusterForMigration = require('./migration-utils').getClusterForMigration;
+const getCacheForMigration = require('./migration-utils').getCacheForMigration;
+
+const _debug = false;
+const DUPLICATE_KEY_ERROR = 11000;
+
+let dup = 1;
+
+function makeDup(name) {
+    return name + `_dup_${dup++}`;
+}
+
+function linkCacheToCluster(clustersModel, cluster, cachesModel, cache, domainsModel) {
+    return clustersModel.updateOne({_id: cluster._id}, {$addToSet: {caches: cache._id}}).exec()
+        .then(() => cachesModel.updateOne({_id: cache._id}, {clusters: [cluster._id]}).exec())
+        .then(() => {
+            if (_.isEmpty(cache.domains))
+                return Promise.resolve();
+
+            return _.reduce(cache.domains, (start, domain) => start.then(() => {
+                return domainsModel.updateOne({_id: domain}, {clusters: [cluster._id]}).exec()
+                    .then(() => clustersModel.updateOne({_id: cluster._id}, {$addToSet: {models: domain}}).exec());
+            }), Promise.resolve());
+        })
+        .catch((err) => error(`Failed link cache to cluster [cache=${cache.name}, cluster=${cluster.name}]`, err));
+}
+
+function cloneCache(clustersModel, cachesModel, domainsModel, cache) {
+    const cacheId = cache._id;
+    const clusters = cache.clusters;
+
+    cache.clusters = [];
+
+    if (cache.cacheStoreFactory && cache.cacheStoreFactory.kind === null)
+        delete cache.cacheStoreFactory.kind;
+
+    return _.reduce(clusters, (start, cluster, idx) => start.then(() => {
+        if (idx > 0) {
+            delete cache._id;
+
+            const newCache = _.clone(cache);
+            const domainIds = newCache.domains;
+
+            newCache.clusters = [cluster];
+            newCache.domains = [];
+
+            return clustersModel.updateMany({_id: {$in: newCache.clusters}}, {$pull: {caches: cacheId}}).exec()
+                .then(() => cachesModel.create(newCache))
+                .catch((err) => {
+                    if (err.code === DUPLICATE_KEY_ERROR) {
+                        const retryWith = makeDup(newCache.name);
+
+                        error(`Failed to clone cache, will change cache name and retry [cache=${newCache.name}, retryWith=${retryWith}]`);
+
+                        newCache.name = retryWith;
+
+                        return cachesModel.create(newCache);
+                    }
+
+                    return Promise.reject(err);
+                })
+                .then((clone) => clustersModel.updateMany({_id: {$in: newCache.clusters}}, {$addToSet: {caches: clone._id}}).exec()
+                    .then(() => clone))
+                .then((clone) => {
+                    if (_.isEmpty(domainIds))
+                        return Promise.resolve();
+
+                    return _.reduce(domainIds, (start, domainId) => start.then(() => {
+                        return domainsModel.findOne({_id: domainId}).lean().exec()
+                            .then((domain) => {
+                                delete domain._id;
+
+                                const newDomain = _.clone(domain);
+
+                                newDomain.caches = [clone._id];
+                                newDomain.clusters = [cluster];
+
+                                return domainsModel.create(newDomain)
+                                    .catch((err) => {
+                                        if (err.code === DUPLICATE_KEY_ERROR) {
+                                            const retryWith = makeDup(newDomain.valueType);
+
+                                            error(`Failed to clone domain, will change type name and retry [cache=${newCache.name}, valueType=${newDomain.valueType}, retryWith=${retryWith}]`);
+
+                                            newDomain.valueType = retryWith;
+
+                                            return domainsModel.create(newDomain);
+                                        }
+                                    })
+                                    .then((createdDomain) => {
+                                        return clustersModel.updateOne({_id: cluster}, {$addToSet: {models: createdDomain._id}}).exec()
+                                            .then(() => cachesModel.updateOne({_id: clone.id}, {$addToSet: {domains: createdDomain._id}}));
+                                    })
+                                    .catch((err) => error('Failed to clone domain during cache clone', err));
+                            })
+                            .catch((err) => error(`Failed to duplicate domain model[domain=${domainId}], cache=${clone.name}]`, err));
+                    }), Promise.resolve());
+                })
+                .catch((err) => error(`Failed to clone cache[id=${cacheId}, name=${cache.name}]`, err));
+        }
+
+        return cachesModel.updateOne({_id: cacheId}, {clusters: [cluster]}).exec()
+            .then(() => clustersModel.updateOne({_id: cluster}, {$addToSet: {models: {$each: cache.domains}}}).exec());
+    }), Promise.resolve());
+}
+
+function migrateCache(clustersModel, cachesModel, domainsModel, cache) {
+    const clustersCnt = _.size(cache.clusters);
+
+    if (clustersCnt < 1) {
+        if (_debug)
+            log(`Found cache not linked to cluster [cache=${cache.name}]`);
+
+        return getClusterForMigration(clustersModel, cache.space)
+            .then((clusterLostFound) => linkCacheToCluster(clustersModel, clusterLostFound, cachesModel, cache, domainsModel));
+    }
+
+    if (clustersCnt > 1) {
+        if (_debug)
+            log(`Found cache linked to many clusters [cache=${cache.name}, clustersCnt=${clustersCnt}]`);
+
+        return cloneCache(clustersModel, cachesModel, domainsModel, cache);
+    }
+
+    // Nothing to migrate, cache linked to cluster 1-to-1.
+    return Promise.resolve();
+}
+
+function migrateCaches(clustersModel, cachesModel, domainsModel) {
+    return cachesModel.find({}).lean().exec()
+        .then((caches) => {
+            const cachesCnt = _.size(caches);
+
+            if (cachesCnt > 0) {
+                log(`Caches to migrate: ${cachesCnt}`);
+
+                return _.reduce(caches, (start, cache) => start.then(() => migrateCache(clustersModel, cachesModel, domainsModel, cache)), Promise.resolve())
+                    .then(() => log('Caches migration finished.'));
+            }
+
+            return Promise.resolve();
+
+        })
+        .catch((err) => error('Caches migration failed', err));
+}
+
+function linkIgfsToCluster(clustersModel, cluster, igfsModel, igfs) {
+    return clustersModel.updateOne({_id: cluster._id}, {$addToSet: {igfss: igfs._id}}).exec()
+        .then(() => igfsModel.updateOne({_id: igfs._id}, {clusters: [cluster._id]}).exec())
+        .catch((err) => error(`Failed link IGFS to cluster [IGFS=${igfs.name}, cluster=${cluster.name}]`, err));
+}
+
+function cloneIgfs(clustersModel, igfsModel, igfs) {
+    const igfsId = igfs._id;
+    const clusters = igfs.clusters;
+
+    delete igfs._id;
+    igfs.clusters = [];
+
+    return _.reduce(clusters, (start, cluster, idx) => start.then(() => {
+        const newIgfs = _.clone(igfs);
+
+        newIgfs.clusters = [cluster];
+
+        if (idx > 0) {
+            return clustersModel.updateMany({_id: {$in: newIgfs.clusters}}, {$pull: {igfss: igfsId}}).exec()
+                .then(() => igfsModel.create(newIgfs))
+                .then((clone) => clustersModel.updateMany({_id: {$in: newIgfs.clusters}}, {$addToSet: {igfss: clone._id}}).exec())
+                .catch((err) => error(`Failed to clone IGFS: id=${igfsId}, name=${igfs.name}]`, err));
+        }
+
+        return igfsModel.updateOne({_id: igfsId}, {clusters: [cluster]}).exec();
+    }), Promise.resolve());
+}
+
+function migrateIgfs(clustersModel, igfsModel, igfs) {
+    const clustersCnt = _.size(igfs.clusters);
+
+    if (clustersCnt < 1) {
+        if (_debug)
+            log(`Found IGFS not linked to cluster [IGFS=${igfs.name}]`);
+
+        return getClusterForMigration(clustersModel, igfs.space)
+            .then((clusterLostFound) => linkIgfsToCluster(clustersModel, clusterLostFound, igfsModel, igfs));
+    }
+
+    if (clustersCnt > 1) {
+        if (_debug)
+            log(`Found IGFS linked to many clusters [IGFS=${igfs.name}, clustersCnt=${clustersCnt}]`);
+
+        return cloneIgfs(clustersModel, igfsModel, igfs);
+    }
+
+    // Nothing to migrate, IGFS linked to cluster 1-to-1.
+    return Promise.resolve();
+}
+
+function migrateIgfss(clustersModel, igfsModel) {
+    return igfsModel.find({}).lean().exec()
+        .then((igfss) => {
+            const igfsCnt = _.size(igfss);
+
+            if (igfsCnt > 0) {
+                log(`IGFS to migrate: ${igfsCnt}`);
+
+                return _.reduce(igfss, (start, igfs) => start.then(() => migrateIgfs(clustersModel, igfsModel, igfs)), Promise.resolve())
+                    .then(() => log('IGFS migration finished.'));
+            }
+
+            return Promise.resolve();
+        })
+        .catch((err) => error('IGFS migration failed', err));
+}
+
+function linkDomainToCluster(clustersModel, cluster, domainsModel, domain) {
+    return clustersModel.updateOne({_id: cluster._id}, {$addToSet: {models: domain._id}}).exec()
+        .then(() => domainsModel.updateOne({_id: domain._id}, {clusters: [cluster._id]}).exec())
+        .catch((err) => error(`Failed link domain model to cluster [domain=${domain._id}, cluster=${cluster.name}]`, err));
+}
+
+function linkDomainToCache(cachesModel, cache, domainsModel, domain) {
+    return cachesModel.updateOne({_id: cache._id}, {$addToSet: {domains: domain._id}}).exec()
+        .then(() => domainsModel.updateOne({_id: domain._id}, {caches: [cache._id]}).exec())
+        .catch((err) => error(`Failed link domain model to cache [cache=${cache.name}, domain=${domain._id}]`, err));
+}
+
+function migrateDomain(clustersModel, cachesModel, domainsModel, domain) {
+    const cachesCnt = _.size(domain.caches);
+
+    if (cachesCnt < 1) {
+        if (_debug)
+            log(`Found domain model not linked to cache [domain=${domain._id}]`);
+
+        return getClusterForMigration(clustersModel, domain.space)
+            .then((clusterLostFound) => linkDomainToCluster(clustersModel, clusterLostFound, domainsModel, domain))
+            .then(() => getCacheForMigration(clustersModel, cachesModel, domain.space))
+            .then((cacheLostFound) => linkDomainToCache(cachesModel, cacheLostFound, domainsModel, domain))
+            .catch((err) => error(`Failed to migrate not linked domain [domain=${domain._id}]`, err));
+    }
+
+    if (_.isEmpty(domain.clusters)) {
+        const cachesCnt = _.size(domain.caches);
+
+        if (_debug)
+            log(`Found domain model without cluster: [domain=${domain._id}, cachesCnt=${cachesCnt}]`);
+
+        const grpByClusters = {};
+
+        return cachesModel.find({_id: {$in: domain.caches}}).lean().exec()
+            .then((caches) => {
+                if (caches) {
+                    _.forEach(caches, (cache) => {
+                        const c = _.get(grpByClusters, cache.clusters[0]);
+
+                        if (c)
+                            c.push(cache._id);
+                        else
+                            grpByClusters[cache.clusters[0]] = [cache._id];
+                    });
+
+                    return _.reduce(_.keys(grpByClusters), (start, cluster, idx) => start.then(() => {
+                        const domainId = domain._id;
+
+                        const clusterCaches = grpByClusters[cluster];
+
+                        if (idx > 0) {
+                            delete domain._id;
+                            domain.caches = clusterCaches;
+
+                            return domainsModel.create(domain)
+                                .then((clonedDomain) => {
+                                    return cachesModel.updateOne({_id: {$in: clusterCaches}}, {$addToSet: {domains: clonedDomain._id}}).exec()
+                                        .then(() => clonedDomain);
+                                })
+                                .then((clonedDomain) => linkDomainToCluster(clustersModel, {_id: cluster, name: `stub${idx}`}, domainsModel, clonedDomain))
+                                .then(() => {
+                                    return cachesModel.updateMany({_id: {$in: clusterCaches}}, {$pull: {domains: domainId}}).exec();
+                                });
+                        }
+
+                        return domainsModel.updateOne({_id: domainId}, {caches: clusterCaches}).exec()
+                            .then(() => linkDomainToCluster(clustersModel, {_id: cluster, name: `stub${idx}`}, domainsModel, domain));
+                    }), Promise.resolve());
+                }
+
+                error(`Found domain with orphaned caches: [domain=${domain._id}, caches=${domain.caches}]`);
+
+                return Promise.resolve();
+            })
+            .catch((err) => error(`Failed to migrate domain [domain=${domain._id}]`, err));
+    }
+
+    // Nothing to migrate, other domains will be migrated with caches.
+    return Promise.resolve();
+}
+
+function migrateDomains(clustersModel, cachesModel, domainsModel) {
+    return domainsModel.find({}).lean().exec()
+        .then((domains) => {
+            const domainsCnt = _.size(domains);
+
+            if (domainsCnt > 0) {
+                log(`Domain models to migrate: ${domainsCnt}`);
+
+                return _.reduce(domains, (start, domain) => start.then(() => migrateDomain(clustersModel, cachesModel, domainsModel, domain)), Promise.resolve())
+                    .then(() => log('Domain models migration finished.'));
+            }
+
+            return Promise.resolve();
+        })
+        .catch((err) => error('Domain models migration failed', err));
+}
+
+function deduplicate(title, model, name) {
+    return model.find({}).lean().exec()
+        .then((items) => {
+            const sz = _.size(items);
+
+            if (sz > 0) {
+                log(`Deduplication of ${title} started...`);
+
+                let cnt = 0;
+
+                return _.reduce(items, (start, item) => start.then(() => {
+                    const data = item[name];
+
+                    const dataSz = _.size(data);
+
+                    if (dataSz < 2)
+                        return Promise.resolve();
+
+                    const deduped = _.uniqWith(data, _.isEqual);
+
+                    if (dataSz !== _.size(deduped)) {
+                        return model.updateOne({_id: item._id}, {$set: {[name]: deduped}})
+                            .then(() => cnt++);
+                    }
+
+                    return Promise.resolve();
+                }), Promise.resolve())
+                    .then(() => log(`Deduplication of ${title} finished: ${cnt}.`));
+            }
+
+            return Promise.resolve();
+        });
+}
+
+exports.up = function up(done) {
+    const clustersModel = this('Cluster');
+    const cachesModel = this('Cache');
+    const domainsModel = this('DomainModel');
+    const igfsModel = this('Igfs');
+
+    process.on('unhandledRejection', function(reason, p) {
+        console.log('Unhandled rejection at:', p, 'reason:', reason);
+    });
+
+    Promise.resolve()
+        .then(() => deduplicate('Cluster caches', clustersModel, 'caches'))
+        .then(() => deduplicate('Cluster IGFS', clustersModel, 'igfss'))
+        .then(() => deduplicate('Cache clusters', cachesModel, 'clusters'))
+        .then(() => deduplicate('Cache domains', cachesModel, 'domains'))
+        .then(() => deduplicate('IGFS clusters', igfsModel, 'clusters'))
+        .then(() => deduplicate('Domain model caches', domainsModel, 'caches'))
+        .then(() => migrateCaches(clustersModel, cachesModel, domainsModel))
+        .then(() => migrateIgfss(clustersModel, igfsModel))
+        .then(() => migrateDomains(clustersModel, cachesModel, domainsModel))
+        .then(() => log(`Duplicates counter: ${dup}`))
+        .then(() => done())
+        .catch(done);
+};
+
+exports.down = function down(done) {
+    log('Model migration can not be reverted');
+
+    done();
+};
diff --git a/modules/backend/migrations/1547440382485-account-make-email-unique.js b/modules/backend/migrations/1547440382485-account-make-email-unique.js
new file mode 100644
index 0000000..3a6e453
--- /dev/null
+++ b/modules/backend/migrations/1547440382485-account-make-email-unique.js
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const _ = require('lodash');
+
+const log = require('./migration-utils').log;
+
+function deduplicateAccounts(model) {
+    const accountsModel = model('Account');
+    const spaceModel = model('Space');
+
+    return accountsModel.aggregate([
+        {$group: {_id: '$email', count: {$sum: 1}}},
+        {$match: {count: {$gt: 1}}}
+    ]).exec()
+        .then((accounts) => _.map(accounts, '_id'))
+        .then((emails) => Promise.all(
+            _.map(emails, (email) => accountsModel.find({email}, {_id: 1, email: 1, lastActivity: 1, lastLogin: 1}).lean().exec())
+        ))
+        .then((promises) => {
+            const duplicates = _.flatMap(promises, (accounts) => _.sortBy(accounts, [(a) => a.lastActivity || '', 'lastLogin']).slice(0, -1));
+
+            if (_.isEmpty(duplicates))
+                log('Duplicates not found!');
+            else {
+                log(`Duplicates found: ${_.size(duplicates)}`);
+
+                _.forEach(duplicates, (dup) => log(`  ID: ${dup._id}, e-mail: ${dup.email}`));
+            }
+
+            return _.map(duplicates, '_id');
+        })
+        .then((accountIds) => {
+            if (_.isEmpty(accountIds))
+                return Promise.resolve();
+
+            return spaceModel.find({owner: {$in: accountIds}}, {_id: 1}).lean().exec()
+                .then((spaces) => _.map(spaces, '_id'))
+                .then((spaceIds) =>
+                    Promise.all([
+                        model('Cluster').deleteMany({space: {$in: spaceIds}}).exec(),
+                        model('Cache').deleteMany({space: {$in: spaceIds}}).exec(),
+                        model('DomainModel').deleteMany({space: {$in: spaceIds}}).exec(),
+                        model('Igfs').deleteMany({space: {$in: spaceIds}}).exec(),
+                        model('Notebook').deleteMany({space: {$in: spaceIds}}).exec(),
+                        model('Activities').deleteMany({owner: accountIds}).exec(),
+                        model('Notifications').deleteMany({owner: accountIds}).exec(),
+                        spaceModel.deleteMany({owner: accountIds}).exec(),
+                        accountsModel.deleteMany({_id: accountIds}).exec()
+                    ])
+                )
+                .then(() => {
+                    const conditions = _.map(accountIds, (accountId) => ({session: {$regex: `"${accountId}"`}}));
+
+                    return accountsModel.db.collection('sessions').deleteMany({$or: conditions});
+                });
+        });
+}
+
+exports.up = function up(done) {
+    deduplicateAccounts((name) => this(name))
+        .then(() => this('Account').collection.createIndex({email: 1}, {unique: true, background: false}))
+        .then(() => done())
+        .catch(done);
+};
+
+exports.down = function down(done) {
+    log('Account migration can not be reverted');
+
+    done();
+};
diff --git a/modules/backend/migrations/README.txt b/modules/backend/migrations/README.txt
new file mode 100644
index 0000000..e907fad
--- /dev/null
+++ b/modules/backend/migrations/README.txt
@@ -0,0 +1,4 @@
+Ignite Web Console
+======================================
+
+This folder contains scripts for model migration.
diff --git a/modules/backend/migrations/migration-utils.js b/modules/backend/migrations/migration-utils.js
new file mode 100644
index 0000000..81c9095
--- /dev/null
+++ b/modules/backend/migrations/migration-utils.js
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+function log(msg) {
+    console.log(`[${new Date().toISOString()}] [INFO ] ${msg}`);
+}
+
+function error(msg, err) {
+    console.log(`[${new Date().toISOString()}] [ERROR] ${msg}.` + (err ? ` Error: ${err}` : ''));
+}
+
+function recreateIndex0(done, model, oldIdxName, oldIdx, newIdx) {
+    return model.indexExists(oldIdxName)
+        .then((exists) => {
+            if (exists) {
+                return model.dropIndex(oldIdx)
+                    .then(() => model.createIndex(newIdx, {unique: true, background: false}));
+            }
+        })
+        .then(() => done())
+        .catch((err) => {
+            if (err.code === 12587) {
+                log(`Background operation in progress for: ${oldIdxName}, will retry in 3 seconds.`);
+
+                setTimeout(() => recreateIndex0(done, model, oldIdxName, oldIdx, newIdx), 3000);
+            }
+            else {
+                log(`Failed to recreate index: ${err}`);
+
+                done();
+            }
+        });
+}
+
+function recreateIndex(done, model, oldIdxName, oldIdx, newIdx) {
+    setTimeout(() => recreateIndex0(done, model, oldIdxName, oldIdx, newIdx), 1000);
+}
+
+const LOST_AND_FOUND = 'LOST_AND_FOUND';
+
+function getClusterForMigration(clustersModel, space) {
+    return clustersModel.findOne({space, name: LOST_AND_FOUND}).lean().exec()
+        .then((cluster) => {
+            if (cluster)
+                return cluster;
+
+            return clustersModel.create({
+                space,
+                name: LOST_AND_FOUND,
+                connector: {noDelay: true},
+                communication: {tcpNoDelay: true},
+                igfss: [],
+                caches: [],
+                binaryConfiguration: {
+                    compactFooter: true,
+                    typeConfigurations: []
+                },
+                discovery: {
+                    kind: 'Multicast',
+                    Multicast: {addresses: ['127.0.0.1:47500..47510']},
+                    Vm: {addresses: ['127.0.0.1:47500..47510']}
+                }
+            });
+        });
+}
+
+function getCacheForMigration(clustersModel, cachesModel, space) {
+    return cachesModel.findOne({space, name: LOST_AND_FOUND})
+        .then((cache) => {
+            if (cache)
+                return cache;
+
+            return getClusterForMigration(clustersModel, space)
+                .then((cluster) => {
+                    return cachesModel.create({
+                        space,
+                        name: LOST_AND_FOUND,
+                        clusters: [cluster._id],
+                        domains: [],
+                        cacheMode: 'PARTITIONED',
+                        atomicityMode: 'ATOMIC',
+                        readFromBackup: true,
+                        copyOnRead: true,
+                        readThrough: false,
+                        writeThrough: false,
+                        sqlFunctionClasses: [],
+                        writeBehindCoalescing: true,
+                        cacheStoreFactory: {
+                            CacheHibernateBlobStoreFactory: {hibernateProperties: []},
+                            CacheJdbcBlobStoreFactory: {connectVia: 'DataSource'}
+                        },
+                        nearConfiguration: {},
+                        evictionPolicy: {}
+                    });
+                })
+                .then((cache) => {
+                    return clustersModel.updateOne({_id: cache.clusters[0]}, {$addToSet: {caches: cache._id}}).exec()
+                        .then(() => cache);
+                });
+        });
+}
+
+module.exports = {
+    log,
+    error,
+    recreateIndex,
+    getClusterForMigration,
+    getCacheForMigration
+};
+
+
+
+
diff --git a/modules/backend/package-lock.json b/modules/backend/package-lock.json
new file mode 100644
index 0000000..28051fa
--- /dev/null
+++ b/modules/backend/package-lock.json
@@ -0,0 +1,8115 @@
+{
+  "name": "ignite-web-console",
+  "version": "8.7.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@babel/code-frame": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
+      "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
+      "dev": true,
+      "requires": {
+        "@babel/highlight": "^7.0.0"
+      }
+    },
+    "@babel/highlight": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
+      "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.0.0",
+        "esutils": "^2.0.2",
+        "js-tokens": "^4.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^1.9.0"
+          }
+        },
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "js-tokens": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+          "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "@babel/parser": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.2.3.tgz",
+      "integrity": "sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA=="
+    },
+    "@mrmlnc/readdir-enhanced": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
+      "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==",
+      "requires": {
+        "call-me-maybe": "^1.0.1",
+        "glob-to-regexp": "^0.3.0"
+      }
+    },
+    "@nodelib/fs.stat": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
+      "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
+    },
+    "accepts": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
+      "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
+      "requires": {
+        "mime-types": "~2.1.18",
+        "negotiator": "0.6.1"
+      }
+    },
+    "acorn": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz",
+      "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==",
+      "dev": true
+    },
+    "acorn-jsx": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz",
+      "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==",
+      "dev": true
+    },
+    "after": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+      "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
+    },
+    "agent-base": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
+      "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
+      "dev": true,
+      "requires": {
+        "es6-promisify": "^5.0.0"
+      }
+    },
+    "ajv": {
+      "version": "6.10.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
+      "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
+      "requires": {
+        "fast-deep-equal": "^2.0.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "ansi-colors": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
+      "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==",
+      "dev": true
+    },
+    "ansi-escapes": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz",
+      "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4="
+    },
+    "ansi-regex": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
+    },
+    "ansi-styles": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+      "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
+    },
+    "anymatch": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
+      "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
+      "optional": true,
+      "requires": {
+        "micromatch": "^2.1.5",
+        "normalize-path": "^2.0.0"
+      }
+    },
+    "app-module-path": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz",
+      "integrity": "sha1-ZBqlXft9am8KgUHEucCqULbCTdU="
+    },
+    "archetype": {
+      "version": "0.8.8",
+      "resolved": "https://registry.npmjs.org/archetype/-/archetype-0.8.8.tgz",
+      "integrity": "sha512-isdIbFfT3zXVan34hmxIwI8A5/8lo9MaYmwXF1iYWCnJS1GvKKnZ4GrXoOUgKdUMCiB/wdguRXeStCUQhFjexg==",
+      "requires": {
+        "lodash.clonedeep": "4.x",
+        "lodash.set": "4.x",
+        "lodash.unset": "4.x",
+        "mpath": "0.5.1",
+        "standard-error": "1.1.0"
+      }
+    },
+    "argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "dev": true,
+      "requires": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "arr-diff": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+      "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+      "optional": true,
+      "requires": {
+        "arr-flatten": "^1.0.1"
+      }
+    },
+    "arr-flatten": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+      "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg=="
+    },
+    "arr-union": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+      "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ="
+    },
+    "array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+    },
+    "array-union": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+      "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+      "requires": {
+        "array-uniq": "^1.0.1"
+      }
+    },
+    "array-uniq": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+      "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY="
+    },
+    "array-unique": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+      "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+      "optional": true
+    },
+    "arraybuffer.slice": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
+      "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
+    },
+    "arrify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+      "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0="
+    },
+    "asn1": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+      "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+      "requires": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+    },
+    "assertion-error": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+      "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+      "dev": true
+    },
+    "assign-symbols": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+      "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c="
+    },
+    "astral-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
+      "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
+      "dev": true
+    },
+    "async": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
+      "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
+      "requires": {
+        "lodash": "^4.17.10"
+      }
+    },
+    "async-each": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
+      "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==",
+      "optional": true
+    },
+    "async-limiter": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
+      "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
+    },
+    "atob": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+      "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
+    },
+    "aws-sign2": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
+    },
+    "aws4": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+      "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
+    },
+    "babel-cli": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-cli/-/babel-cli-6.26.0.tgz",
+      "integrity": "sha1-UCq1SHTX24itALiHoGODzgPQAvE=",
+      "requires": {
+        "babel-core": "^6.26.0",
+        "babel-polyfill": "^6.26.0",
+        "babel-register": "^6.26.0",
+        "babel-runtime": "^6.26.0",
+        "chokidar": "^1.6.1",
+        "commander": "^2.11.0",
+        "convert-source-map": "^1.5.0",
+        "fs-readdir-recursive": "^1.0.0",
+        "glob": "^7.1.2",
+        "lodash": "^4.17.4",
+        "output-file-sync": "^1.1.2",
+        "path-is-absolute": "^1.0.1",
+        "slash": "^1.0.0",
+        "source-map": "^0.5.6",
+        "v8flags": "^2.1.1"
+      }
+    },
+    "babel-code-frame": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+      "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+      "requires": {
+        "chalk": "^1.1.3",
+        "esutils": "^2.0.2",
+        "js-tokens": "^3.0.2"
+      }
+    },
+    "babel-core": {
+      "version": "6.26.3",
+      "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz",
+      "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==",
+      "requires": {
+        "babel-code-frame": "^6.26.0",
+        "babel-generator": "^6.26.0",
+        "babel-helpers": "^6.24.1",
+        "babel-messages": "^6.23.0",
+        "babel-register": "^6.26.0",
+        "babel-runtime": "^6.26.0",
+        "babel-template": "^6.26.0",
+        "babel-traverse": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "babylon": "^6.18.0",
+        "convert-source-map": "^1.5.1",
+        "debug": "^2.6.9",
+        "json5": "^0.5.1",
+        "lodash": "^4.17.4",
+        "minimatch": "^3.0.4",
+        "path-is-absolute": "^1.0.1",
+        "private": "^0.1.8",
+        "slash": "^1.0.0",
+        "source-map": "^0.5.7"
+      },
+      "dependencies": {
+        "minimatch": {
+          "version": "3.0.4",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        }
+      }
+    },
+    "babel-generator": {
+      "version": "6.26.1",
+      "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz",
+      "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==",
+      "requires": {
+        "babel-messages": "^6.23.0",
+        "babel-runtime": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "detect-indent": "^4.0.0",
+        "jsesc": "^1.3.0",
+        "lodash": "^4.17.4",
+        "source-map": "^0.5.7",
+        "trim-right": "^1.0.1"
+      }
+    },
+    "babel-helper-builder-binary-assignment-operator-visitor": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz",
+      "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=",
+      "requires": {
+        "babel-helper-explode-assignable-expression": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helper-call-delegate": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz",
+      "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=",
+      "requires": {
+        "babel-helper-hoist-variables": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-traverse": "^6.24.1",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helper-define-map": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz",
+      "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=",
+      "requires": {
+        "babel-helper-function-name": "^6.24.1",
+        "babel-runtime": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-helper-explode-assignable-expression": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz",
+      "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=",
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-traverse": "^6.24.1",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helper-function-name": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz",
+      "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=",
+      "requires": {
+        "babel-helper-get-function-arity": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1",
+        "babel-traverse": "^6.24.1",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helper-get-function-arity": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz",
+      "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=",
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helper-hoist-variables": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz",
+      "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=",
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helper-optimise-call-expression": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz",
+      "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=",
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helper-regex": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz",
+      "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=",
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-helper-remap-async-to-generator": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz",
+      "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=",
+      "requires": {
+        "babel-helper-function-name": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1",
+        "babel-traverse": "^6.24.1",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helper-replace-supers": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz",
+      "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=",
+      "requires": {
+        "babel-helper-optimise-call-expression": "^6.24.1",
+        "babel-messages": "^6.23.0",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1",
+        "babel-traverse": "^6.24.1",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helpers": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz",
+      "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=",
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1"
+      }
+    },
+    "babel-messages": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz",
+      "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=",
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-check-es2015-constants": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz",
+      "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=",
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-syntax-async-functions": {
+      "version": "6.13.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz",
+      "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU="
+    },
+    "babel-plugin-syntax-exponentiation-operator": {
+      "version": "6.13.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz",
+      "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4="
+    },
+    "babel-plugin-syntax-trailing-function-commas": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz",
+      "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM="
+    },
+    "babel-plugin-transform-async-to-generator": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz",
+      "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=",
+      "requires": {
+        "babel-helper-remap-async-to-generator": "^6.24.1",
+        "babel-plugin-syntax-async-functions": "^6.8.0",
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-arrow-functions": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz",
+      "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=",
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-block-scoped-functions": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz",
+      "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=",
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-block-scoping": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz",
+      "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=",
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "babel-template": "^6.26.0",
+        "babel-traverse": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-plugin-transform-es2015-classes": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz",
+      "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=",
+      "requires": {
+        "babel-helper-define-map": "^6.24.1",
+        "babel-helper-function-name": "^6.24.1",
+        "babel-helper-optimise-call-expression": "^6.24.1",
+        "babel-helper-replace-supers": "^6.24.1",
+        "babel-messages": "^6.23.0",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1",
+        "babel-traverse": "^6.24.1",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-computed-properties": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz",
+      "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=",
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-destructuring": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz",
+      "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=",
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-duplicate-keys": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz",
+      "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=",
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-for-of": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz",
+      "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=",
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-function-name": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz",
+      "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=",
+      "requires": {
+        "babel-helper-function-name": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-literals": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz",
+      "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=",
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-modules-amd": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz",
+      "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=",
+      "requires": {
+        "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-modules-commonjs": {
+      "version": "6.26.2",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz",
+      "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==",
+      "requires": {
+        "babel-plugin-transform-strict-mode": "^6.24.1",
+        "babel-runtime": "^6.26.0",
+        "babel-template": "^6.26.0",
+        "babel-types": "^6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-modules-systemjs": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz",
+      "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=",
+      "requires": {
+        "babel-helper-hoist-variables": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-modules-umd": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz",
+      "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=",
+      "requires": {
+        "babel-plugin-transform-es2015-modules-amd": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-object-super": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz",
+      "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=",
+      "requires": {
+        "babel-helper-replace-supers": "^6.24.1",
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-parameters": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz",
+      "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=",
+      "requires": {
+        "babel-helper-call-delegate": "^6.24.1",
+        "babel-helper-get-function-arity": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1",
+        "babel-traverse": "^6.24.1",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-shorthand-properties": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz",
+      "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=",
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-spread": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz",
+      "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=",
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-sticky-regex": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz",
+      "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=",
+      "requires": {
+        "babel-helper-regex": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-template-literals": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz",
+      "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=",
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-typeof-symbol": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz",
+      "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=",
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-unicode-regex": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz",
+      "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=",
+      "requires": {
+        "babel-helper-regex": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "regexpu-core": "^2.0.0"
+      }
+    },
+    "babel-plugin-transform-exponentiation-operator": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz",
+      "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=",
+      "requires": {
+        "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1",
+        "babel-plugin-syntax-exponentiation-operator": "^6.8.0",
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-regenerator": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz",
+      "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=",
+      "requires": {
+        "regenerator-transform": "^0.10.0"
+      }
+    },
+    "babel-plugin-transform-runtime": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz",
+      "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=",
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-strict-mode": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz",
+      "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=",
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-polyfill": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz",
+      "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=",
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "core-js": "^2.5.0",
+        "regenerator-runtime": "^0.10.5"
+      },
+      "dependencies": {
+        "regenerator-runtime": {
+          "version": "0.10.5",
+          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
+          "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg="
+        }
+      }
+    },
+    "babel-preset-es2015": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz",
+      "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=",
+      "requires": {
+        "babel-plugin-check-es2015-constants": "^6.22.0",
+        "babel-plugin-transform-es2015-arrow-functions": "^6.22.0",
+        "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0",
+        "babel-plugin-transform-es2015-block-scoping": "^6.24.1",
+        "babel-plugin-transform-es2015-classes": "^6.24.1",
+        "babel-plugin-transform-es2015-computed-properties": "^6.24.1",
+        "babel-plugin-transform-es2015-destructuring": "^6.22.0",
+        "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1",
+        "babel-plugin-transform-es2015-for-of": "^6.22.0",
+        "babel-plugin-transform-es2015-function-name": "^6.24.1",
+        "babel-plugin-transform-es2015-literals": "^6.22.0",
+        "babel-plugin-transform-es2015-modules-amd": "^6.24.1",
+        "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1",
+        "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1",
+        "babel-plugin-transform-es2015-modules-umd": "^6.24.1",
+        "babel-plugin-transform-es2015-object-super": "^6.24.1",
+        "babel-plugin-transform-es2015-parameters": "^6.24.1",
+        "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1",
+        "babel-plugin-transform-es2015-spread": "^6.22.0",
+        "babel-plugin-transform-es2015-sticky-regex": "^6.24.1",
+        "babel-plugin-transform-es2015-template-literals": "^6.22.0",
+        "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0",
+        "babel-plugin-transform-es2015-unicode-regex": "^6.24.1",
+        "babel-plugin-transform-regenerator": "^6.24.1"
+      }
+    },
+    "babel-preset-es2016": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-preset-es2016/-/babel-preset-es2016-6.24.1.tgz",
+      "integrity": "sha1-+QC/k+LrwNJ235uKtZck6/2Vn4s=",
+      "requires": {
+        "babel-plugin-transform-exponentiation-operator": "^6.24.1"
+      }
+    },
+    "babel-preset-es2017": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-preset-es2017/-/babel-preset-es2017-6.24.1.tgz",
+      "integrity": "sha1-WXvq37n38gi8/YoS6bKym4svFNE=",
+      "requires": {
+        "babel-plugin-syntax-trailing-function-commas": "^6.22.0",
+        "babel-plugin-transform-async-to-generator": "^6.24.1"
+      }
+    },
+    "babel-preset-latest": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-preset-latest/-/babel-preset-latest-6.24.1.tgz",
+      "integrity": "sha1-Z33gaRVKdIXC0lxXfAL2JLhbheg=",
+      "requires": {
+        "babel-preset-es2015": "^6.24.1",
+        "babel-preset-es2016": "^6.24.1",
+        "babel-preset-es2017": "^6.24.1"
+      }
+    },
+    "babel-register": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz",
+      "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=",
+      "requires": {
+        "babel-core": "^6.26.0",
+        "babel-runtime": "^6.26.0",
+        "core-js": "^2.5.0",
+        "home-or-tmp": "^2.0.0",
+        "lodash": "^4.17.4",
+        "mkdirp": "^0.5.1",
+        "source-map-support": "^0.4.15"
+      }
+    },
+    "babel-runtime": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+      "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+      "requires": {
+        "core-js": "^2.4.0",
+        "regenerator-runtime": "^0.11.0"
+      }
+    },
+    "babel-template": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz",
+      "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=",
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "babel-traverse": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "babylon": "^6.18.0",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-traverse": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz",
+      "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=",
+      "requires": {
+        "babel-code-frame": "^6.26.0",
+        "babel-messages": "^6.23.0",
+        "babel-runtime": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "babylon": "^6.18.0",
+        "debug": "^2.6.8",
+        "globals": "^9.18.0",
+        "invariant": "^2.2.2",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-types": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz",
+      "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=",
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "esutils": "^2.0.2",
+        "lodash": "^4.17.4",
+        "to-fast-properties": "^1.0.3"
+      }
+    },
+    "babylon": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
+      "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ=="
+    },
+    "backo2": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+      "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+    },
+    "base": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+      "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+      "requires": {
+        "cache-base": "^1.0.1",
+        "class-utils": "^0.3.5",
+        "component-emitter": "^1.2.1",
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.1",
+        "mixin-deep": "^1.2.0",
+        "pascalcase": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
+        }
+      }
+    },
+    "base64-arraybuffer": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+      "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
+    },
+    "base64-js": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
+      "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw=="
+    },
+    "base64id": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+      "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY="
+    },
+    "basic-auth": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+      "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+      "requires": {
+        "safe-buffer": "5.1.2"
+      }
+    },
+    "bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+      "requires": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
+    "better-assert": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+      "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+      "requires": {
+        "callsite": "1.0.0"
+      }
+    },
+    "binary-extensions": {
+      "version": "1.13.1",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
+      "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
+      "optional": true
+    },
+    "bl": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
+      "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==",
+      "requires": {
+        "readable-stream": "^2.3.5",
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "blob": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
+      "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
+    },
+    "bluebird": {
+      "version": "3.5.4",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz",
+      "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw=="
+    },
+    "body-parser": {
+      "version": "1.19.0",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+      "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
+      "requires": {
+        "bytes": "3.1.0",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "http-errors": "1.7.2",
+        "iconv-lite": "0.4.24",
+        "on-finished": "~2.3.0",
+        "qs": "6.7.0",
+        "raw-body": "2.4.0",
+        "type-is": "~1.6.17"
+      }
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
+      "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+      "optional": true,
+      "requires": {
+        "expand-range": "^1.8.1",
+        "preserve": "^0.2.0",
+        "repeat-element": "^1.1.2"
+      }
+    },
+    "browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+      "dev": true
+    },
+    "bson": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.1.tgz",
+      "integrity": "sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg=="
+    },
+    "buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz",
+      "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==",
+      "requires": {
+        "base64-js": "^1.0.2",
+        "ieee754": "^1.1.4"
+      }
+    },
+    "buffer-alloc": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
+      "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
+      "requires": {
+        "buffer-alloc-unsafe": "^1.1.0",
+        "buffer-fill": "^1.0.0"
+      }
+    },
+    "buffer-alloc-unsafe": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
+      "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
+    },
+    "buffer-crc32": {
+      "version": "0.2.13",
+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+      "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
+    },
+    "buffer-fill": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
+      "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw="
+    },
+    "buffer-from": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
+    },
+    "byline": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz",
+      "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE="
+    },
+    "bytes": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+      "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
+    },
+    "cache-base": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+      "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+      "requires": {
+        "collection-visit": "^1.0.0",
+        "component-emitter": "^1.2.1",
+        "get-value": "^2.0.6",
+        "has-value": "^1.0.0",
+        "isobject": "^3.0.1",
+        "set-value": "^2.0.0",
+        "to-object-path": "^0.3.0",
+        "union-value": "^1.0.0",
+        "unset-value": "^1.0.0"
+      },
+      "dependencies": {
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
+        }
+      }
+    },
+    "call-me-maybe": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz",
+      "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms="
+    },
+    "callsite": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+      "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
+    },
+    "callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true
+    },
+    "camelcase": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+      "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo="
+    },
+    "caseless": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
+    },
+    "chai": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
+      "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
+      "dev": true,
+      "requires": {
+        "assertion-error": "^1.1.0",
+        "check-error": "^1.0.2",
+        "deep-eql": "^3.0.1",
+        "get-func-name": "^2.0.0",
+        "pathval": "^1.1.0",
+        "type-detect": "^4.0.5"
+      }
+    },
+    "chalk": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+      "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+      "requires": {
+        "ansi-styles": "^2.2.1",
+        "escape-string-regexp": "^1.0.2",
+        "has-ansi": "^2.0.0",
+        "strip-ansi": "^3.0.0",
+        "supports-color": "^2.0.0"
+      }
+    },
+    "chardet": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+      "dev": true
+    },
+    "check-error": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+      "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
+      "dev": true
+    },
+    "chokidar": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
+      "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
+      "optional": true,
+      "requires": {
+        "anymatch": "^1.3.0",
+        "async-each": "^1.0.0",
+        "fsevents": "^1.0.0",
+        "glob-parent": "^2.0.0",
+        "inherits": "^2.0.1",
+        "is-binary-path": "^1.0.0",
+        "is-glob": "^2.0.0",
+        "path-is-absolute": "^1.0.0",
+        "readdirp": "^2.0.0"
+      }
+    },
+    "class-utils": {
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+      "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+      "requires": {
+        "arr-union": "^3.1.0",
+        "define-property": "^0.2.5",
+        "isobject": "^3.0.0",
+        "static-extend": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
+        }
+      }
+    },
+    "cli-cursor": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz",
+      "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
+      "requires": {
+        "restore-cursor": "^1.0.1"
+      }
+    },
+    "cli-width": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
+      "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk="
+    },
+    "cliui": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+      "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+      "requires": {
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.1",
+        "wrap-ansi": "^2.0.0"
+      }
+    },
+    "code-point-at": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+      "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
+    },
+    "collection-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+      "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+      "requires": {
+        "map-visit": "^1.0.0",
+        "object-visit": "^1.0.0"
+      }
+    },
+    "color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "requires": {
+        "color-name": "1.1.3"
+      }
+    },
+    "color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+    },
+    "colors": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz",
+      "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg=="
+    },
+    "combined-stream": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
+      "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
+      "requires": {
+        "delayed-stream": "~1.0.0"
+      }
+    },
+    "commander": {
+      "version": "2.20.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
+      "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ=="
+    },
+    "commondir": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+      "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
+      "dev": true
+    },
+    "component-bind": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+      "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
+    },
+    "component-emitter": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
+      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
+    },
+    "component-inherit": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+      "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+    },
+    "concat-stream": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+      "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.2.2",
+        "typedarray": "^0.0.6"
+      }
+    },
+    "connect-mongodb-session": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/connect-mongodb-session/-/connect-mongodb-session-2.1.1.tgz",
+      "integrity": "sha512-k8NF+C32tJZuR3sSFfdz56e1NKzxejWlD/X5PVYBIZQ8/dzqkMGTKxcdGXsBreU5M48WZ+vYGrJSruSRTzFu4Q==",
+      "requires": {
+        "archetype": "0.8.x",
+        "mongodb": "~3.1.8"
+      }
+    },
+    "content-disposition": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+      "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
+    },
+    "content-type": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+    },
+    "convert-source-map": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
+      "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==",
+      "requires": {
+        "safe-buffer": "~5.1.1"
+      }
+    },
+    "cookie": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+    },
+    "cookie-parser": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz",
+      "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==",
+      "requires": {
+        "cookie": "0.3.1",
+        "cookie-signature": "1.0.6"
+      }
+    },
+    "cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+    },
+    "cookiejar": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
+      "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==",
+      "dev": true
+    },
+    "copy-descriptor": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+      "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
+    },
+    "core-js": {
+      "version": "2.6.5",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
+      "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A=="
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+    },
+    "cross-env": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz",
+      "integrity": "sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==",
+      "dev": true,
+      "requires": {
+        "cross-spawn": "^6.0.5",
+        "is-windows": "^1.0.0"
+      }
+    },
+    "cross-spawn": {
+      "version": "6.0.5",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+      "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+      "dev": true,
+      "requires": {
+        "nice-try": "^1.0.4",
+        "path-key": "^2.0.1",
+        "semver": "^5.5.0",
+        "shebang-command": "^1.2.0",
+        "which": "^1.2.9"
+      }
+    },
+    "dashdash": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "requires": {
+        "ms": "2.0.0"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
+    },
+    "decode-uri-component": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+      "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
+    },
+    "decompress": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz",
+      "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=",
+      "requires": {
+        "decompress-tar": "^4.0.0",
+        "decompress-tarbz2": "^4.0.0",
+        "decompress-targz": "^4.0.0",
+        "decompress-unzip": "^4.0.1",
+        "graceful-fs": "^4.1.10",
+        "make-dir": "^1.0.0",
+        "pify": "^2.3.0",
+        "strip-dirs": "^2.0.0"
+      }
+    },
+    "decompress-tar": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz",
+      "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==",
+      "requires": {
+        "file-type": "^5.2.0",
+        "is-stream": "^1.1.0",
+        "tar-stream": "^1.5.2"
+      }
+    },
+    "decompress-tarbz2": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz",
+      "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==",
+      "requires": {
+        "decompress-tar": "^4.1.0",
+        "file-type": "^6.1.0",
+        "is-stream": "^1.1.0",
+        "seek-bzip": "^1.0.5",
+        "unbzip2-stream": "^1.0.9"
+      },
+      "dependencies": {
+        "file-type": {
+          "version": "6.2.0",
+          "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz",
+          "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg=="
+        }
+      }
+    },
+    "decompress-targz": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz",
+      "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==",
+      "requires": {
+        "decompress-tar": "^4.1.1",
+        "file-type": "^5.2.0",
+        "is-stream": "^1.1.0"
+      }
+    },
+    "decompress-unzip": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz",
+      "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=",
+      "requires": {
+        "file-type": "^3.8.0",
+        "get-stream": "^2.2.0",
+        "pify": "^2.3.0",
+        "yauzl": "^2.4.2"
+      },
+      "dependencies": {
+        "file-type": {
+          "version": "3.9.0",
+          "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
+          "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek="
+        }
+      }
+    },
+    "dedent": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
+      "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
+      "dev": true
+    },
+    "deep-eql": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+      "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
+      "dev": true,
+      "requires": {
+        "type-detect": "^4.0.0"
+      }
+    },
+    "deep-is": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
+    },
+    "define-properties": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+      "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+      "dev": true,
+      "requires": {
+        "object-keys": "^1.0.12"
+      }
+    },
+    "define-property": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+      "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+      "requires": {
+        "is-descriptor": "^1.0.2",
+        "isobject": "^3.0.1"
+      },
+      "dependencies": {
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
+        }
+      }
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
+    },
+    "depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+    },
+    "destroy": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+      "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+    },
+    "detect-indent": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+      "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+      "requires": {
+        "repeating": "^2.0.0"
+      }
+    },
+    "diff": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="
+    },
+    "dir-glob": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz",
+      "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==",
+      "requires": {
+        "arrify": "^1.0.1",
+        "path-type": "^3.0.0"
+      },
+      "dependencies": {
+        "path-type": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+          "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+          "requires": {
+            "pify": "^3.0.0"
+          }
+        },
+        "pify": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+          "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
+        }
+      }
+    },
+    "doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2"
+      }
+    },
+    "dotenv": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz",
+      "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow=="
+    },
+    "ecc-jsbn": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+      "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+      "requires": {
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
+    "ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+    },
+    "emoji-regex": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+      "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+      "dev": true
+    },
+    "encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+    },
+    "end-of-stream": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
+      "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
+      "requires": {
+        "once": "^1.4.0"
+      }
+    },
+    "engine.io": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz",
+      "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==",
+      "requires": {
+        "accepts": "~1.3.4",
+        "base64id": "1.0.0",
+        "cookie": "0.3.1",
+        "debug": "~3.1.0",
+        "engine.io-parser": "~2.1.0",
+        "ws": "~3.3.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "engine.io-client": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
+      "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==",
+      "requires": {
+        "component-emitter": "1.2.1",
+        "component-inherit": "0.0.3",
+        "debug": "~3.1.0",
+        "engine.io-parser": "~2.1.1",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "ws": "~3.3.1",
+        "xmlhttprequest-ssl": "~1.5.4",
+        "yeast": "0.1.2"
+      },
+      "dependencies": {
+        "component-emitter": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+          "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "engine.io-parser": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz",
+      "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==",
+      "requires": {
+        "after": "0.8.2",
+        "arraybuffer.slice": "~0.0.7",
+        "base64-arraybuffer": "0.1.5",
+        "blob": "0.0.5",
+        "has-binary2": "~1.0.2"
+      }
+    },
+    "error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "requires": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "es-abstract": {
+      "version": "1.13.0",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz",
+      "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==",
+      "dev": true,
+      "requires": {
+        "es-to-primitive": "^1.2.0",
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3",
+        "is-callable": "^1.1.4",
+        "is-regex": "^1.0.4",
+        "object-keys": "^1.0.12"
+      }
+    },
+    "es-to-primitive": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz",
+      "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==",
+      "dev": true,
+      "requires": {
+        "is-callable": "^1.1.4",
+        "is-date-object": "^1.0.1",
+        "is-symbol": "^1.0.2"
+      }
+    },
+    "es6-promise": {
+      "version": "4.2.6",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz",
+      "integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==",
+      "dev": true
+    },
+    "es6-promisify": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+      "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
+      "dev": true,
+      "requires": {
+        "es6-promise": "^4.0.3"
+      }
+    },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+    },
+    "escodegen": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz",
+      "integrity": "sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==",
+      "requires": {
+        "esprima": "^3.1.3",
+        "estraverse": "^4.2.0",
+        "esutils": "^2.0.2",
+        "optionator": "^0.8.1",
+        "source-map": "~0.6.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "optional": true
+        }
+      }
+    },
+    "eslint": {
+      "version": "5.16.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz",
+      "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "ajv": "^6.9.1",
+        "chalk": "^2.1.0",
+        "cross-spawn": "^6.0.5",
+        "debug": "^4.0.1",
+        "doctrine": "^3.0.0",
+        "eslint-scope": "^4.0.3",
+        "eslint-utils": "^1.3.1",
+        "eslint-visitor-keys": "^1.0.0",
+        "espree": "^5.0.1",
+        "esquery": "^1.0.1",
+        "esutils": "^2.0.2",
+        "file-entry-cache": "^5.0.1",
+        "functional-red-black-tree": "^1.0.1",
+        "glob": "^7.1.2",
+        "globals": "^11.7.0",
+        "ignore": "^4.0.6",
+        "import-fresh": "^3.0.0",
+        "imurmurhash": "^0.1.4",
+        "inquirer": "^6.2.2",
+        "js-yaml": "^3.13.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.3.0",
+        "lodash": "^4.17.11",
+        "minimatch": "^3.0.4",
+        "mkdirp": "^0.5.1",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.8.2",
+        "path-is-inside": "^1.0.2",
+        "progress": "^2.0.0",
+        "regexpp": "^2.0.1",
+        "semver": "^5.5.1",
+        "strip-ansi": "^4.0.0",
+        "strip-json-comments": "^2.0.1",
+        "table": "^5.2.3",
+        "text-table": "^0.2.0"
+      },
+      "dependencies": {
+        "ansi-escapes": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
+          "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
+          "dev": true
+        },
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^1.9.0"
+          }
+        },
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "cli-cursor": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+          "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+          "dev": true,
+          "requires": {
+            "restore-cursor": "^2.0.0"
+          }
+        },
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "figures": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+          "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+          "dev": true,
+          "requires": {
+            "escape-string-regexp": "^1.0.5"
+          }
+        },
+        "globals": {
+          "version": "11.11.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz",
+          "integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==",
+          "dev": true
+        },
+        "ignore": {
+          "version": "4.0.6",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+          "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+          "dev": true
+        },
+        "inquirer": {
+          "version": "6.3.1",
+          "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz",
+          "integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==",
+          "dev": true,
+          "requires": {
+            "ansi-escapes": "^3.2.0",
+            "chalk": "^2.4.2",
+            "cli-cursor": "^2.1.0",
+            "cli-width": "^2.0.0",
+            "external-editor": "^3.0.3",
+            "figures": "^2.0.0",
+            "lodash": "^4.17.11",
+            "mute-stream": "0.0.7",
+            "run-async": "^2.2.0",
+            "rxjs": "^6.4.0",
+            "string-width": "^2.1.0",
+            "strip-ansi": "^5.1.0",
+            "through": "^2.3.6"
+          },
+          "dependencies": {
+            "ansi-regex": {
+              "version": "4.1.0",
+              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+              "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+              "dev": true
+            },
+            "strip-ansi": {
+              "version": "5.2.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+              "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+              "dev": true,
+              "requires": {
+                "ansi-regex": "^4.1.0"
+              }
+            }
+          }
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+          "dev": true
+        },
+        "mute-stream": {
+          "version": "0.0.7",
+          "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+          "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
+          "dev": true
+        },
+        "onetime": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+          "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+          "dev": true,
+          "requires": {
+            "mimic-fn": "^1.0.0"
+          }
+        },
+        "restore-cursor": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+          "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+          "dev": true,
+          "requires": {
+            "onetime": "^2.0.0",
+            "signal-exit": "^3.0.2"
+          }
+        },
+        "run-async": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
+          "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+          "dev": true,
+          "requires": {
+            "is-promise": "^2.1.0"
+          }
+        },
+        "string-width": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+          "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+          "dev": true,
+          "requires": {
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^4.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "eslint-formatter-friendly": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-formatter-friendly/-/eslint-formatter-friendly-6.0.0.tgz",
+      "integrity": "sha512-fOBwGn2r8BPQ1KSKyVzjXP8VFxJ2tWKxxn2lIF+k1ezN/pFB44HDlrn5kBm1vxbyyRa/LC+1vHJwc7WETUAZ2Q==",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "6.26.0",
+        "chalk": "^2.0.1",
+        "extend": "^3.0.0",
+        "strip-ansi": "^4.0.0",
+        "text-table": "^0.2.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^1.9.0"
+          }
+        },
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "eslint-scope": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
+      "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
+      "dev": true,
+      "requires": {
+        "esrecurse": "^4.1.0",
+        "estraverse": "^4.1.1"
+      }
+    },
+    "eslint-utils": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
+      "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==",
+      "dev": true
+    },
+    "eslint-visitor-keys": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
+      "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==",
+      "dev": true
+    },
+    "espree": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz",
+      "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==",
+      "dev": true,
+      "requires": {
+        "acorn": "^6.0.7",
+        "acorn-jsx": "^5.0.0",
+        "eslint-visitor-keys": "^1.0.0"
+      }
+    },
+    "esprima": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
+      "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM="
+    },
+    "esquery": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz",
+      "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.0.0"
+      }
+    },
+    "esrecurse": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+      "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.1.0"
+      }
+    },
+    "estraverse": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
+      "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM="
+    },
+    "esutils": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs="
+    },
+    "etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+    },
+    "execa": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+      "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
+      "dev": true,
+      "requires": {
+        "cross-spawn": "^6.0.0",
+        "get-stream": "^4.0.0",
+        "is-stream": "^1.1.0",
+        "npm-run-path": "^2.0.0",
+        "p-finally": "^1.0.0",
+        "signal-exit": "^3.0.0",
+        "strip-eof": "^1.0.0"
+      },
+      "dependencies": {
+        "get-stream": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+          "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+          "dev": true,
+          "requires": {
+            "pump": "^3.0.0"
+          }
+        }
+      }
+    },
+    "exit-hook": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
+      "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g="
+    },
+    "expand-brackets": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
+      "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+      "optional": true,
+      "requires": {
+        "is-posix-bracket": "^0.1.0"
+      }
+    },
+    "expand-range": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz",
+      "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=",
+      "optional": true,
+      "requires": {
+        "fill-range": "^2.1.0"
+      }
+    },
+    "expand-template": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-1.1.1.tgz",
+      "integrity": "sha512-cebqLtV8KOZfw0UI8TEFWxtczxxC1jvyUvx6H4fyp1K1FN7A4Q+uggVUlOsI1K8AGU0rwOGqP8nCapdrw8CYQg=="
+    },
+    "express": {
+      "version": "4.16.4",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
+      "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
+      "requires": {
+        "accepts": "~1.3.5",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.18.3",
+        "content-disposition": "0.5.2",
+        "content-type": "~1.0.4",
+        "cookie": "0.3.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "1.1.1",
+        "fresh": "0.5.2",
+        "merge-descriptors": "1.0.1",
+        "methods": "~1.1.2",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.2",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "~2.0.4",
+        "qs": "6.5.2",
+        "range-parser": "~1.2.0",
+        "safe-buffer": "5.1.2",
+        "send": "0.16.2",
+        "serve-static": "1.13.2",
+        "setprototypeof": "1.1.0",
+        "statuses": "~1.4.0",
+        "type-is": "~1.6.16",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "dependencies": {
+        "body-parser": {
+          "version": "1.18.3",
+          "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
+          "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
+          "requires": {
+            "bytes": "3.0.0",
+            "content-type": "~1.0.4",
+            "debug": "2.6.9",
+            "depd": "~1.1.2",
+            "http-errors": "~1.6.3",
+            "iconv-lite": "0.4.23",
+            "on-finished": "~2.3.0",
+            "qs": "6.5.2",
+            "raw-body": "2.3.3",
+            "type-is": "~1.6.16"
+          }
+        },
+        "bytes": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+          "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
+        },
+        "http-errors": {
+          "version": "1.6.3",
+          "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+          "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+          "requires": {
+            "depd": "~1.1.2",
+            "inherits": "2.0.3",
+            "setprototypeof": "1.1.0",
+            "statuses": ">= 1.4.0 < 2"
+          }
+        },
+        "iconv-lite": {
+          "version": "0.4.23",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
+          "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3"
+          }
+        },
+        "qs": {
+          "version": "6.5.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+          "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+        },
+        "raw-body": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
+          "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
+          "requires": {
+            "bytes": "3.0.0",
+            "http-errors": "1.6.3",
+            "iconv-lite": "0.4.23",
+            "unpipe": "1.0.0"
+          }
+        },
+        "setprototypeof": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+          "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
+        },
+        "statuses": {
+          "version": "1.4.0",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+          "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
+        }
+      }
+    },
+    "express-mongo-sanitize": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/express-mongo-sanitize/-/express-mongo-sanitize-1.3.2.tgz",
+      "integrity": "sha1-+6QE9sBBV3y+7sTdkFfO+7Q53lo="
+    },
+    "express-session": {
+      "version": "1.16.1",
+      "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.16.1.tgz",
+      "integrity": "sha512-pWvUL8Tl5jUy1MLH7DhgUlpoKeVPUTe+y6WQD9YhcN0C5qAhsh4a8feVjiUXo3TFhIy191YGZ4tewW9edbl2xQ==",
+      "requires": {
+        "cookie": "0.3.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "~2.0.0",
+        "on-headers": "~1.0.2",
+        "parseurl": "~1.3.2",
+        "safe-buffer": "5.1.2",
+        "uid-safe": "~2.1.5"
+      },
+      "dependencies": {
+        "depd": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+          "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+        }
+      }
+    },
+    "extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
+    },
+    "extend-shallow": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+      "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+      "requires": {
+        "assign-symbols": "^1.0.0",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "external-editor": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz",
+      "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==",
+      "dev": true,
+      "requires": {
+        "chardet": "^0.7.0",
+        "iconv-lite": "^0.4.24",
+        "tmp": "^0.0.33"
+      }
+    },
+    "extglob": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
+      "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+      "optional": true,
+      "requires": {
+        "is-extglob": "^1.0.0"
+      }
+    },
+    "extsprintf": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
+    },
+    "fast-deep-equal": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+      "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
+    },
+    "fast-glob": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.6.tgz",
+      "integrity": "sha512-0BvMaZc1k9F+MeWWMe8pL6YltFzZYcJsYU7D4JyDA6PAczaXvxqQQ/z+mDF7/4Mw01DeUc+i3CTKajnkANkV4w==",
+      "requires": {
+        "@mrmlnc/readdir-enhanced": "^2.2.1",
+        "@nodelib/fs.stat": "^1.1.2",
+        "glob-parent": "^3.1.0",
+        "is-glob": "^4.0.0",
+        "merge2": "^1.2.3",
+        "micromatch": "^3.1.10"
+      },
+      "dependencies": {
+        "arr-diff": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+          "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA="
+        },
+        "array-unique": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+          "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg="
+        },
+        "braces": {
+          "version": "2.3.2",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+          "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+          "requires": {
+            "arr-flatten": "^1.1.0",
+            "array-unique": "^0.3.2",
+            "extend-shallow": "^2.0.1",
+            "fill-range": "^4.0.0",
+            "isobject": "^3.0.1",
+            "repeat-element": "^1.1.2",
+            "snapdragon": "^0.8.1",
+            "snapdragon-node": "^2.0.1",
+            "split-string": "^3.0.2",
+            "to-regex": "^3.0.1"
+          },
+          "dependencies": {
+            "extend-shallow": {
+              "version": "2.0.1",
+              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+              "requires": {
+                "is-extendable": "^0.1.0"
+              }
+            }
+          }
+        },
+        "expand-brackets": {
+          "version": "2.1.4",
+          "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+          "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+          "requires": {
+            "debug": "^2.3.3",
+            "define-property": "^0.2.5",
+            "extend-shallow": "^2.0.1",
+            "posix-character-classes": "^0.1.0",
+            "regex-not": "^1.0.0",
+            "snapdragon": "^0.8.1",
+            "to-regex": "^3.0.1"
+          },
+          "dependencies": {
+            "define-property": {
+              "version": "0.2.5",
+              "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+              "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+              "requires": {
+                "is-descriptor": "^0.1.0"
+              }
+            },
+            "extend-shallow": {
+              "version": "2.0.1",
+              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+              "requires": {
+                "is-extendable": "^0.1.0"
+              }
+            },
+            "is-accessor-descriptor": {
+              "version": "0.1.6",
+              "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+              "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+              "requires": {
+                "kind-of": "^3.0.2"
+              },
+              "dependencies": {
+                "kind-of": {
+                  "version": "3.2.2",
+                  "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+                  "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+                  "requires": {
+                    "is-buffer": "^1.1.5"
+                  }
+                }
+              }
+            },
+            "is-data-descriptor": {
+              "version": "0.1.4",
+              "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+              "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+              "requires": {
+                "kind-of": "^3.0.2"
+              },
+              "dependencies": {
+                "kind-of": {
+                  "version": "3.2.2",
+                  "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+                  "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+                  "requires": {
+                    "is-buffer": "^1.1.5"
+                  }
+                }
+              }
+            },
+            "is-descriptor": {
+              "version": "0.1.6",
+              "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+              "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+              "requires": {
+                "is-accessor-descriptor": "^0.1.6",
+                "is-data-descriptor": "^0.1.4",
+                "kind-of": "^5.0.0"
+              }
+            },
+            "kind-of": {
+              "version": "5.1.0",
+              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+              "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw=="
+            }
+          }
+        },
+        "extglob": {
+          "version": "2.0.4",
+          "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+          "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+          "requires": {
+            "array-unique": "^0.3.2",
+            "define-property": "^1.0.0",
+            "expand-brackets": "^2.1.4",
+            "extend-shallow": "^2.0.1",
+            "fragment-cache": "^0.2.1",
+            "regex-not": "^1.0.0",
+            "snapdragon": "^0.8.1",
+            "to-regex": "^3.0.1"
+          },
+          "dependencies": {
+            "define-property": {
+              "version": "1.0.0",
+              "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+              "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+              "requires": {
+                "is-descriptor": "^1.0.0"
+              }
+            },
+            "extend-shallow": {
+              "version": "2.0.1",
+              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+              "requires": {
+                "is-extendable": "^0.1.0"
+              }
+            }
+          }
+        },
+        "fill-range": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+          "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+          "requires": {
+            "extend-shallow": "^2.0.1",
+            "is-number": "^3.0.0",
+            "repeat-string": "^1.6.1",
+            "to-regex-range": "^2.1.0"
+          },
+          "dependencies": {
+            "extend-shallow": {
+              "version": "2.0.1",
+              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+              "requires": {
+                "is-extendable": "^0.1.0"
+              }
+            }
+          }
+        },
+        "glob-parent": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+          "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+          "requires": {
+            "is-glob": "^3.1.0",
+            "path-dirname": "^1.0.0"
+          },
+          "dependencies": {
+            "is-glob": {
+              "version": "3.1.0",
+              "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+              "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+              "requires": {
+                "is-extglob": "^2.1.0"
+              }
+            }
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        },
+        "is-extglob": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+          "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
+        },
+        "is-glob": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+          "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+          "requires": {
+            "is-extglob": "^2.1.1"
+          }
+        },
+        "is-number": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+          "requires": {
+            "kind-of": "^3.0.2"
+          },
+          "dependencies": {
+            "kind-of": {
+              "version": "3.2.2",
+              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+              "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+              "requires": {
+                "is-buffer": "^1.1.5"
+              }
+            }
+          }
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
+        },
+        "micromatch": {
+          "version": "3.1.10",
+          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+          "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+          "requires": {
+            "arr-diff": "^4.0.0",
+            "array-unique": "^0.3.2",
+            "braces": "^2.3.1",
+            "define-property": "^2.0.2",
+            "extend-shallow": "^3.0.2",
+            "extglob": "^2.0.4",
+            "fragment-cache": "^0.2.1",
+            "kind-of": "^6.0.2",
+            "nanomatch": "^1.2.9",
+            "object.pick": "^1.3.0",
+            "regex-not": "^1.0.0",
+            "snapdragon": "^0.8.1",
+            "to-regex": "^3.0.2"
+          }
+        }
+      }
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
+    },
+    "fd-slicer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+      "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
+      "requires": {
+        "pend": "~1.2.0"
+      }
+    },
+    "figures": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
+      "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
+      "requires": {
+        "escape-string-regexp": "^1.0.5",
+        "object-assign": "^4.1.0"
+      }
+    },
+    "file-entry-cache": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz",
+      "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==",
+      "dev": true,
+      "requires": {
+        "flat-cache": "^2.0.1"
+      }
+    },
+    "file-type": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz",
+      "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY="
+    },
+    "filename-regex": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
+      "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=",
+      "optional": true
+    },
+    "fill-range": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz",
+      "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==",
+      "optional": true,
+      "requires": {
+        "is-number": "^2.1.0",
+        "isobject": "^2.0.0",
+        "randomatic": "^3.0.0",
+        "repeat-element": "^1.1.2",
+        "repeat-string": "^1.5.2"
+      }
+    },
+    "finalhandler": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
+      "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
+      "requires": {
+        "debug": "2.6.9",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.2",
+        "statuses": "~1.4.0",
+        "unpipe": "~1.0.0"
+      },
+      "dependencies": {
+        "statuses": {
+          "version": "1.4.0",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+          "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
+        }
+      }
+    },
+    "find-cache-dir": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz",
+      "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==",
+      "dev": true,
+      "requires": {
+        "commondir": "^1.0.1",
+        "make-dir": "^2.0.0",
+        "pkg-dir": "^3.0.0"
+      },
+      "dependencies": {
+        "make-dir": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+          "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+          "dev": true,
+          "requires": {
+            "pify": "^4.0.1",
+            "semver": "^5.6.0"
+          }
+        },
+        "pify": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+          "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+          "dev": true
+        }
+      }
+    },
+    "find-package-json": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/find-package-json/-/find-package-json-1.2.0.tgz",
+      "integrity": "sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw==",
+      "dev": true
+    },
+    "find-up": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+      "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+      "requires": {
+        "path-exists": "^2.0.0",
+        "pinkie-promise": "^2.0.0"
+      }
+    },
+    "fire-up": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fire-up/-/fire-up-1.0.0.tgz",
+      "integrity": "sha1-cyXaXgqCyH77X39wM6uil1EB/0I=",
+      "requires": {
+        "bluebird": "^3.3.4",
+        "lodash": "^4.7.0",
+        "simple-glob": "0.1.1"
+      }
+    },
+    "flat": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz",
+      "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==",
+      "dev": true,
+      "requires": {
+        "is-buffer": "~2.0.3"
+      },
+      "dependencies": {
+        "is-buffer": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
+          "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==",
+          "dev": true
+        }
+      }
+    },
+    "flat-cache": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
+      "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==",
+      "dev": true,
+      "requires": {
+        "flatted": "^2.0.0",
+        "rimraf": "2.6.3",
+        "write": "1.0.3"
+      }
+    },
+    "flatted": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz",
+      "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==",
+      "dev": true
+    },
+    "for-in": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+      "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA="
+    },
+    "for-own": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
+      "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=",
+      "optional": true,
+      "requires": {
+        "for-in": "^1.0.1"
+      }
+    },
+    "forever-agent": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
+    },
+    "form-data": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+      "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.6",
+        "mime-types": "^2.1.12"
+      }
+    },
+    "formidable": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz",
+      "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==",
+      "dev": true
+    },
+    "forwarded": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+      "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+    },
+    "fragment-cache": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+      "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+      "requires": {
+        "map-cache": "^0.2.2"
+      }
+    },
+    "fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+    },
+    "from2": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
+      "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
+      "requires": {
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.0"
+      }
+    },
+    "fs-constants": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+    },
+    "fs-extra": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-2.1.2.tgz",
+      "integrity": "sha1-BGxwFjzvmq1GsOSn+kZ/si1x3jU=",
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "jsonfile": "^2.1.0"
+      }
+    },
+    "fs-readdir-recursive": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
+      "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA=="
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+    },
+    "fsevents": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.8.tgz",
+      "integrity": "sha512-tPvHgPGB7m40CZ68xqFGkKuzN+RnpGmSV+hgeKxhRpbxdqKXUFJGC3yonBOLzQBcJyGpdZFDfCsdOC2KFsXzeA==",
+      "optional": true,
+      "requires": {
+        "nan": "^2.12.1",
+        "node-pre-gyp": "^0.12.0"
+      },
+      "dependencies": {
+        "abbrev": {
+          "version": "1.1.1",
+          "bundled": true,
+          "optional": true
+        },
+        "ansi-regex": {
+          "version": "2.1.1",
+          "bundled": true
+        },
+        "aproba": {
+          "version": "1.2.0",
+          "bundled": true,
+          "optional": true
+        },
+        "are-we-there-yet": {
+          "version": "1.1.5",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "delegates": "^1.0.0",
+            "readable-stream": "^2.0.6"
+          }
+        },
+        "balanced-match": {
+          "version": "1.0.0",
+          "bundled": true
+        },
+        "brace-expansion": {
+          "version": "1.1.11",
+          "bundled": true,
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "chownr": {
+          "version": "1.1.1",
+          "bundled": true,
+          "optional": true
+        },
+        "code-point-at": {
+          "version": "1.1.0",
+          "bundled": true
+        },
+        "concat-map": {
+          "version": "0.0.1",
+          "bundled": true
+        },
+        "console-control-strings": {
+          "version": "1.1.0",
+          "bundled": true
+        },
+        "core-util-is": {
+          "version": "1.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "debug": {
+          "version": "4.1.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "deep-extend": {
+          "version": "0.6.0",
+          "bundled": true,
+          "optional": true
+        },
+        "delegates": {
+          "version": "1.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "detect-libc": {
+          "version": "1.0.3",
+          "bundled": true,
+          "optional": true
+        },
+        "fs-minipass": {
+          "version": "1.2.5",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "minipass": "^2.2.1"
+          }
+        },
+        "fs.realpath": {
+          "version": "1.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "gauge": {
+          "version": "2.7.4",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "aproba": "^1.0.3",
+            "console-control-strings": "^1.0.0",
+            "has-unicode": "^2.0.0",
+            "object-assign": "^4.1.0",
+            "signal-exit": "^3.0.0",
+            "string-width": "^1.0.1",
+            "strip-ansi": "^3.0.1",
+            "wide-align": "^1.1.0"
+          }
+        },
+        "glob": {
+          "version": "7.1.3",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "has-unicode": {
+          "version": "2.0.1",
+          "bundled": true,
+          "optional": true
+        },
+        "iconv-lite": {
+          "version": "0.4.24",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3"
+          }
+        },
+        "ignore-walk": {
+          "version": "3.0.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "minimatch": "^3.0.4"
+          }
+        },
+        "inflight": {
+          "version": "1.0.6",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "once": "^1.3.0",
+            "wrappy": "1"
+          }
+        },
+        "inherits": {
+          "version": "2.0.3",
+          "bundled": true
+        },
+        "ini": {
+          "version": "1.3.5",
+          "bundled": true,
+          "optional": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "bundled": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "isarray": {
+          "version": "1.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "bundled": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
+        "minimist": {
+          "version": "0.0.8",
+          "bundled": true
+        },
+        "minipass": {
+          "version": "2.3.5",
+          "bundled": true,
+          "requires": {
+            "safe-buffer": "^5.1.2",
+            "yallist": "^3.0.0"
+          }
+        },
+        "minizlib": {
+          "version": "1.2.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "minipass": "^2.2.1"
+          }
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "bundled": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "bundled": true,
+          "optional": true
+        },
+        "needle": {
+          "version": "2.3.0",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "debug": "^4.1.0",
+            "iconv-lite": "^0.4.4",
+            "sax": "^1.2.4"
+          }
+        },
+        "node-pre-gyp": {
+          "version": "0.12.0",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "detect-libc": "^1.0.2",
+            "mkdirp": "^0.5.1",
+            "needle": "^2.2.1",
+            "nopt": "^4.0.1",
+            "npm-packlist": "^1.1.6",
+            "npmlog": "^4.0.2",
+            "rc": "^1.2.7",
+            "rimraf": "^2.6.1",
+            "semver": "^5.3.0",
+            "tar": "^4"
+          }
+        },
+        "nopt": {
+          "version": "4.0.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "abbrev": "1",
+            "osenv": "^0.1.4"
+          }
+        },
+        "npm-bundled": {
+          "version": "1.0.6",
+          "bundled": true,
+          "optional": true
+        },
+        "npm-packlist": {
+          "version": "1.4.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "ignore-walk": "^3.0.1",
+            "npm-bundled": "^1.0.1"
+          }
+        },
+        "npmlog": {
+          "version": "4.1.2",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "are-we-there-yet": "~1.1.2",
+            "console-control-strings": "~1.1.0",
+            "gauge": "~2.7.3",
+            "set-blocking": "~2.0.0"
+          }
+        },
+        "number-is-nan": {
+          "version": "1.0.1",
+          "bundled": true
+        },
+        "object-assign": {
+          "version": "4.1.1",
+          "bundled": true,
+          "optional": true
+        },
+        "once": {
+          "version": "1.4.0",
+          "bundled": true,
+          "requires": {
+            "wrappy": "1"
+          }
+        },
+        "os-homedir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "os-tmpdir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "osenv": {
+          "version": "0.1.5",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "os-homedir": "^1.0.0",
+            "os-tmpdir": "^1.0.0"
+          }
+        },
+        "path-is-absolute": {
+          "version": "1.0.1",
+          "bundled": true,
+          "optional": true
+        },
+        "process-nextick-args": {
+          "version": "2.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "rc": {
+          "version": "1.2.8",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "deep-extend": "^0.6.0",
+            "ini": "~1.3.0",
+            "minimist": "^1.2.0",
+            "strip-json-comments": "~2.0.1"
+          },
+          "dependencies": {
+            "minimist": {
+              "version": "1.2.0",
+              "bundled": true,
+              "optional": true
+            }
+          }
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "rimraf": {
+          "version": "2.6.3",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "glob": "^7.1.3"
+          }
+        },
+        "safe-buffer": {
+          "version": "5.1.2",
+          "bundled": true
+        },
+        "safer-buffer": {
+          "version": "2.1.2",
+          "bundled": true,
+          "optional": true
+        },
+        "sax": {
+          "version": "1.2.4",
+          "bundled": true,
+          "optional": true
+        },
+        "semver": {
+          "version": "5.7.0",
+          "bundled": true,
+          "optional": true
+        },
+        "set-blocking": {
+          "version": "2.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "signal-exit": {
+          "version": "3.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "bundled": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "bundled": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "strip-json-comments": {
+          "version": "2.0.1",
+          "bundled": true,
+          "optional": true
+        },
+        "tar": {
+          "version": "4.4.8",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "chownr": "^1.1.1",
+            "fs-minipass": "^1.2.5",
+            "minipass": "^2.3.4",
+            "minizlib": "^1.1.1",
+            "mkdirp": "^0.5.0",
+            "safe-buffer": "^5.1.2",
+            "yallist": "^3.0.2"
+          }
+        },
+        "util-deprecate": {
+          "version": "1.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "wide-align": {
+          "version": "1.1.3",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "string-width": "^1.0.2 || 2"
+          }
+        },
+        "wrappy": {
+          "version": "1.0.2",
+          "bundled": true
+        },
+        "yallist": {
+          "version": "3.0.3",
+          "bundled": true
+        }
+      }
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "dev": true
+    },
+    "functional-red-black-tree": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+      "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+      "dev": true
+    },
+    "generaterr": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/generaterr/-/generaterr-1.5.0.tgz",
+      "integrity": "sha1-sM62zFFk3yoGEzjMNAqGFTlcUvw="
+    },
+    "get-caller-file": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+      "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w=="
+    },
+    "get-func-name": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+      "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
+      "dev": true
+    },
+    "get-port": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz",
+      "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==",
+      "dev": true
+    },
+    "get-stream": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz",
+      "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=",
+      "requires": {
+        "object-assign": "^4.0.1",
+        "pinkie-promise": "^2.0.0"
+      }
+    },
+    "get-value": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+      "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg="
+    },
+    "getos": {
+      "version": "2.8.4",
+      "resolved": "https://registry.npmjs.org/getos/-/getos-2.8.4.tgz",
+      "integrity": "sha1-e4YD02GcKOOMsP56T2PDrLgNUWM=",
+      "requires": {
+        "async": "2.1.4"
+      },
+      "dependencies": {
+        "async": {
+          "version": "2.1.4",
+          "resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz",
+          "integrity": "sha1-LSFgx3iAMuTdbL4lAvH5osj2zeQ=",
+          "requires": {
+            "lodash": "^4.14.0"
+          }
+        }
+      }
+    },
+    "getpass": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "glob": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+      "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "dependencies": {
+        "minimatch": {
+          "version": "3.0.4",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        }
+      }
+    },
+    "glob-base": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz",
+      "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=",
+      "optional": true,
+      "requires": {
+        "glob-parent": "^2.0.0",
+        "is-glob": "^2.0.0"
+      }
+    },
+    "glob-parent": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+      "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+      "requires": {
+        "is-glob": "^2.0.0"
+      }
+    },
+    "glob-to-regexp": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz",
+      "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs="
+    },
+    "globals": {
+      "version": "9.18.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
+      "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ=="
+    },
+    "globby": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz",
+      "integrity": "sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==",
+      "requires": {
+        "array-union": "^1.0.1",
+        "dir-glob": "2.0.0",
+        "fast-glob": "^2.0.2",
+        "glob": "^7.1.2",
+        "ignore": "^3.3.5",
+        "pify": "^3.0.0",
+        "slash": "^1.0.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+          "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
+        }
+      }
+    },
+    "graceful-fs": {
+      "version": "4.1.15",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
+      "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA=="
+    },
+    "graceful-readlink": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
+      "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU="
+    },
+    "growl": {
+      "version": "1.10.5",
+      "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+      "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+      "dev": true
+    },
+    "har-schema": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
+    },
+    "har-validator": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
+      "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
+      "requires": {
+        "ajv": "^6.5.5",
+        "har-schema": "^2.0.0"
+      }
+    },
+    "has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dev": true,
+      "requires": {
+        "function-bind": "^1.1.1"
+      }
+    },
+    "has-ansi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+      "requires": {
+        "ansi-regex": "^2.0.0"
+      }
+    },
+    "has-binary2": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
+      "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
+      "requires": {
+        "isarray": "2.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+        }
+      }
+    },
+    "has-cors": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+      "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+    },
+    "has-symbols": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
+      "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
+      "dev": true
+    },
+    "has-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+      "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+      "requires": {
+        "get-value": "^2.0.6",
+        "has-values": "^1.0.0",
+        "isobject": "^3.0.0"
+      },
+      "dependencies": {
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
+        }
+      }
+    },
+    "has-values": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+      "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+      "requires": {
+        "is-number": "^3.0.0",
+        "kind-of": "^4.0.0"
+      },
+      "dependencies": {
+        "is-number": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+          "requires": {
+            "kind-of": "^3.0.2"
+          },
+          "dependencies": {
+            "kind-of": {
+              "version": "3.2.2",
+              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+              "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+              "requires": {
+                "is-buffer": "^1.1.5"
+              }
+            }
+          }
+        },
+        "kind-of": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+          "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true
+    },
+    "home-or-tmp": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
+      "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=",
+      "requires": {
+        "os-homedir": "^1.0.0",
+        "os-tmpdir": "^1.0.1"
+      }
+    },
+    "hosted-git-info": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
+      "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w=="
+    },
+    "http-errors": {
+      "version": "1.7.2",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+      "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
+      "requires": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.3",
+        "setprototypeof": "1.1.1",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.0"
+      }
+    },
+    "http-signature": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+      "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "jsprim": "^1.2.2",
+        "sshpk": "^1.7.0"
+      }
+    },
+    "https-proxy-agent": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
+      "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
+      "dev": true,
+      "requires": {
+        "agent-base": "^4.1.0",
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+          "dev": true
+        }
+      }
+    },
+    "iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      }
+    },
+    "ieee754": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+      "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
+    },
+    "ignore": {
+      "version": "3.3.10",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
+      "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug=="
+    },
+    "immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
+    },
+    "import-fresh": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz",
+      "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==",
+      "dev": true,
+      "requires": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "dependencies": {
+        "resolve-from": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+          "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+          "dev": true
+        }
+      }
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+      "dev": true
+    },
+    "in-publish": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz",
+      "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E="
+    },
+    "indexof": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+      "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+    },
+    "ini": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+      "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
+    },
+    "inquirer": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz",
+      "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=",
+      "requires": {
+        "ansi-escapes": "^1.1.0",
+        "ansi-regex": "^2.0.0",
+        "chalk": "^1.0.0",
+        "cli-cursor": "^1.0.1",
+        "cli-width": "^2.0.0",
+        "figures": "^1.3.5",
+        "lodash": "^4.3.0",
+        "readline2": "^1.0.1",
+        "run-async": "^0.1.0",
+        "rx-lite": "^3.1.2",
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.0",
+        "through": "^2.3.6"
+      }
+    },
+    "into-stream": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-4.0.0.tgz",
+      "integrity": "sha512-i29KNyE5r0Y/UQzcQ0IbZO1MYJ53Jn0EcFRZPj5FzWKYH17kDFEOwuA+3jroymOI06SW1dEDnly9A1CAreC5dg==",
+      "requires": {
+        "from2": "^2.1.1",
+        "p-is-promise": "^2.0.0"
+      }
+    },
+    "invariant": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+      "requires": {
+        "loose-envify": "^1.0.0"
+      }
+    },
+    "invert-kv": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
+      "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY="
+    },
+    "ipaddr.js": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
+      "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA=="
+    },
+    "is-accessor-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+      "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+      "requires": {
+        "kind-of": "^3.0.2"
+      }
+    },
+    "is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
+    },
+    "is-binary-path": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+      "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+      "optional": true,
+      "requires": {
+        "binary-extensions": "^1.0.0"
+      }
+    },
+    "is-buffer": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
+    },
+    "is-callable": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
+      "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
+      "dev": true
+    },
+    "is-data-descriptor": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+      "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+      "requires": {
+        "kind-of": "^3.0.2"
+      }
+    },
+    "is-date-object": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
+      "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
+      "dev": true
+    },
+    "is-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "requires": {
+        "is-accessor-descriptor": "^0.1.6",
+        "is-data-descriptor": "^0.1.4",
+        "kind-of": "^5.0.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+          "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw=="
+        }
+      }
+    },
+    "is-dotfile": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz",
+      "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=",
+      "optional": true
+    },
+    "is-equal-shallow": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz",
+      "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=",
+      "optional": true,
+      "requires": {
+        "is-primitive": "^2.0.0"
+      }
+    },
+    "is-extendable": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+      "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik="
+    },
+    "is-extglob": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+      "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA="
+    },
+    "is-finite": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+      "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+      "requires": {
+        "number-is-nan": "^1.0.0"
+      }
+    },
+    "is-fullwidth-code-point": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+      "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+      "requires": {
+        "number-is-nan": "^1.0.0"
+      }
+    },
+    "is-glob": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+      "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+      "requires": {
+        "is-extglob": "^1.0.0"
+      }
+    },
+    "is-natural-number": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz",
+      "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg="
+    },
+    "is-number": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
+      "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
+      "optional": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      }
+    },
+    "is-plain-object": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+      "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+      "requires": {
+        "isobject": "^3.0.1"
+      },
+      "dependencies": {
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
+        }
+      }
+    },
+    "is-posix-bracket": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz",
+      "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=",
+      "optional": true
+    },
+    "is-primitive": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz",
+      "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=",
+      "optional": true
+    },
+    "is-promise": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
+      "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
+      "dev": true
+    },
+    "is-regex": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
+      "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
+      "dev": true,
+      "requires": {
+        "has": "^1.0.1"
+      }
+    },
+    "is-stream": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
+    },
+    "is-symbol": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz",
+      "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==",
+      "dev": true,
+      "requires": {
+        "has-symbols": "^1.0.0"
+      }
+    },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+    },
+    "is-utf8": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+      "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI="
+    },
+    "is-windows": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+      "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="
+    },
+    "isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "dev": true
+    },
+    "isobject": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+      "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+      "optional": true,
+      "requires": {
+        "isarray": "1.0.0"
+      }
+    },
+    "isstream": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
+    },
+    "js-tokens": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
+    },
+    "js-yaml": {
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+      "dev": true,
+      "requires": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      },
+      "dependencies": {
+        "esprima": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+          "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+          "dev": true
+        }
+      }
+    },
+    "jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
+    },
+    "jsesc": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
+      "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s="
+    },
+    "json-schema": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
+    },
+    "json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+    },
+    "json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+      "dev": true
+    },
+    "json-stringify-safe": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
+    },
+    "json5": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+      "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE="
+    },
+    "jsonfile": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
+      "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
+      "requires": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "jsprim": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+      "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+      "requires": {
+        "assert-plus": "1.0.0",
+        "extsprintf": "1.3.0",
+        "json-schema": "0.2.3",
+        "verror": "1.10.0"
+      }
+    },
+    "jszip": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.1.tgz",
+      "integrity": "sha512-iCMBbo4eE5rb1VCpm5qXOAaUiRKRUKiItn8ah2YQQx9qymmSAY98eyQfioChEYcVQLh0zxJ3wS4A0mh90AVPvw==",
+      "requires": {
+        "lie": "~3.3.0",
+        "pako": "~1.0.2",
+        "readable-stream": "~2.3.6",
+        "set-immediate-shim": "~1.0.1"
+      }
+    },
+    "kareem": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.0.tgz",
+      "integrity": "sha512-6hHxsp9e6zQU8nXsP+02HGWXwTkOEw6IROhF2ZA28cYbUk4eJ6QbtZvdqZOdD9YPKghG3apk5eOCvs+tLl3lRg=="
+    },
+    "kind-of": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+      "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+      "requires": {
+        "is-buffer": "^1.1.5"
+      }
+    },
+    "lcid": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
+      "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
+      "requires": {
+        "invert-kv": "^1.0.0"
+      }
+    },
+    "levn": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+      "requires": {
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2"
+      }
+    },
+    "lie": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+      "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+      "requires": {
+        "immediate": "~3.0.5"
+      }
+    },
+    "load-json-file": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+      "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "parse-json": "^2.2.0",
+        "pify": "^2.0.0",
+        "pinkie-promise": "^2.0.0",
+        "strip-bom": "^2.0.0"
+      }
+    },
+    "locate-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+      "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+      "dev": true,
+      "requires": {
+        "p-locate": "^3.0.0",
+        "path-exists": "^3.0.0"
+      },
+      "dependencies": {
+        "path-exists": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+          "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+          "dev": true
+        }
+      }
+    },
+    "lockfile": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz",
+      "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==",
+      "dev": true,
+      "requires": {
+        "signal-exit": "^3.0.2"
+      }
+    },
+    "lodash": {
+      "version": "4.17.11",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
+    },
+    "lodash.assign": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
+      "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc="
+    },
+    "lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
+    },
+    "lodash.set": {
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
+      "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM="
+    },
+    "lodash.unset": {
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/lodash.unset/-/lodash.unset-4.5.2.tgz",
+      "integrity": "sha1-Nw0dPoW3Kn4bDN8tJyEhMG8j5O0="
+    },
+    "log-symbols": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
+      "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.0.1"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^1.9.0"
+          }
+        },
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "requires": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      }
+    },
+    "lru-cache": {
+      "version": "2.7.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz",
+      "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI="
+    },
+    "make-dir": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
+      "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
+      "requires": {
+        "pify": "^3.0.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+          "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
+        }
+      }
+    },
+    "make-error": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
+      "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g=="
+    },
+    "map-age-cleaner": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
+      "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
+      "dev": true,
+      "requires": {
+        "p-defer": "^1.0.0"
+      }
+    },
+    "map-cache": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+      "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8="
+    },
+    "map-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+      "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+      "requires": {
+        "object-visit": "^1.0.0"
+      }
+    },
+    "math-random": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz",
+      "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==",
+      "optional": true
+    },
+    "md5-file": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-3.1.1.tgz",
+      "integrity": "sha1-2zySwJu9pcLeiD+lSQ3XEf3burk="
+    },
+    "media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+    },
+    "mem": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
+      "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==",
+      "dev": true,
+      "requires": {
+        "map-age-cleaner": "^0.1.1",
+        "mimic-fn": "^2.0.0",
+        "p-is-promise": "^2.0.0"
+      },
+      "dependencies": {
+        "mimic-fn": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+          "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+          "dev": true
+        }
+      }
+    },
+    "memory-pager": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+      "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+      "optional": true
+    },
+    "merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+    },
+    "merge2": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.3.tgz",
+      "integrity": "sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA=="
+    },
+    "methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+    },
+    "micromatch": {
+      "version": "2.3.11",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
+      "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
+      "optional": true,
+      "requires": {
+        "arr-diff": "^2.0.0",
+        "array-unique": "^0.2.1",
+        "braces": "^1.8.2",
+        "expand-brackets": "^0.1.4",
+        "extglob": "^0.3.1",
+        "filename-regex": "^2.0.0",
+        "is-extglob": "^1.0.0",
+        "is-glob": "^2.0.1",
+        "kind-of": "^3.0.2",
+        "normalize-path": "^2.0.1",
+        "object.omit": "^2.0.0",
+        "parse-glob": "^3.0.4",
+        "regex-cache": "^0.4.2"
+      }
+    },
+    "migrate-mongoose-typescript": {
+      "version": "3.3.4",
+      "resolved": "https://registry.npmjs.org/migrate-mongoose-typescript/-/migrate-mongoose-typescript-3.3.4.tgz",
+      "integrity": "sha512-0CE8HldEfCNWhQzLOBmn7QqNR1Pb9ynaW5w1BABuUkIYTH7uI08lYq0YUd1OgZuk9xPfzV5SYBxATbR2jFiRrA==",
+      "requires": {
+        "babel-cli": "^6.26.0",
+        "babel-core": "^6.26.3",
+        "babel-plugin-transform-runtime": "^6.23.0",
+        "babel-polyfill": "^6.26.0",
+        "babel-preset-latest": "^6.24.1",
+        "babel-register": "^6.26.0",
+        "bluebird": "^3.5.1",
+        "colors": "^1.2.4",
+        "dotenv": "^5.0.1",
+        "inquirer": "^0.12.0",
+        "mkdirp": "^0.5.1",
+        "mongoose": "^5.3.2",
+        "ts-node": "^6.0.3",
+        "yargs": "^4.8.1"
+      }
+    },
+    "mime": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
+      "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
+    },
+    "mime-db": {
+      "version": "1.40.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+      "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
+    },
+    "mime-types": {
+      "version": "2.1.24",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+      "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
+      "requires": {
+        "mime-db": "1.40.0"
+      }
+    },
+    "mimic-fn": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+      "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+      "dev": true
+    },
+    "minimatch": {
+      "version": "0.2.14",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
+      "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=",
+      "requires": {
+        "lru-cache": "2",
+        "sigmund": "~1.0.0"
+      }
+    },
+    "minimist": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
+    },
+    "mixin-deep": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
+      "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+      "requires": {
+        "for-in": "^1.0.2",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "mkdirp": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "requires": {
+        "minimist": "0.0.8"
+      }
+    },
+    "mocha": {
+      "version": "6.1.4",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz",
+      "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==",
+      "dev": true,
+      "requires": {
+        "ansi-colors": "3.2.3",
+        "browser-stdout": "1.3.1",
+        "debug": "3.2.6",
+        "diff": "3.5.0",
+        "escape-string-regexp": "1.0.5",
+        "find-up": "3.0.0",
+        "glob": "7.1.3",
+        "growl": "1.10.5",
+        "he": "1.2.0",
+        "js-yaml": "3.13.1",
+        "log-symbols": "2.2.0",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.1",
+        "ms": "2.1.1",
+        "node-environment-flags": "1.0.5",
+        "object.assign": "4.1.0",
+        "strip-json-comments": "2.0.1",
+        "supports-color": "6.0.0",
+        "which": "1.3.1",
+        "wide-align": "1.1.3",
+        "yargs": "13.2.2",
+        "yargs-parser": "13.0.0",
+        "yargs-unparser": "1.5.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "camelcase": {
+          "version": "5.3.1",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+          "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+          "dev": true
+        },
+        "cliui": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
+          "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
+          "dev": true,
+          "requires": {
+            "string-width": "^2.1.1",
+            "strip-ansi": "^4.0.0",
+            "wrap-ansi": "^2.0.0"
+          },
+          "dependencies": {
+            "string-width": {
+              "version": "2.1.1",
+              "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+              "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+              "dev": true,
+              "requires": {
+                "is-fullwidth-code-point": "^2.0.0",
+                "strip-ansi": "^4.0.0"
+              }
+            }
+          }
+        },
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "get-caller-file": {
+          "version": "2.0.5",
+          "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+          "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+          "dev": true
+        },
+        "invert-kv": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
+          "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "lcid": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
+          "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
+          "dev": true,
+          "requires": {
+            "invert-kv": "^2.0.0"
+          }
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+          "dev": true
+        },
+        "os-locale": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
+          "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
+          "dev": true,
+          "requires": {
+            "execa": "^1.0.0",
+            "lcid": "^2.0.0",
+            "mem": "^4.0.0"
+          }
+        },
+        "require-main-filename": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+          "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+          "dev": true
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          },
+          "dependencies": {
+            "ansi-regex": {
+              "version": "4.1.0",
+              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+              "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+              "dev": true
+            },
+            "strip-ansi": {
+              "version": "5.2.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+              "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+              "dev": true,
+              "requires": {
+                "ansi-regex": "^4.1.0"
+              }
+            }
+          }
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
+          "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        },
+        "which-module": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+          "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+          "dev": true
+        },
+        "y18n": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+          "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
+          "dev": true
+        },
+        "yargs": {
+          "version": "13.2.2",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz",
+          "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==",
+          "dev": true,
+          "requires": {
+            "cliui": "^4.0.0",
+            "find-up": "^3.0.0",
+            "get-caller-file": "^2.0.1",
+            "os-locale": "^3.1.0",
+            "require-directory": "^2.1.1",
+            "require-main-filename": "^2.0.0",
+            "set-blocking": "^2.0.0",
+            "string-width": "^3.0.0",
+            "which-module": "^2.0.0",
+            "y18n": "^4.0.0",
+            "yargs-parser": "^13.0.0"
+          }
+        },
+        "yargs-parser": {
+          "version": "13.0.0",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz",
+          "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==",
+          "dev": true,
+          "requires": {
+            "camelcase": "^5.0.0",
+            "decamelize": "^1.2.0"
+          }
+        }
+      }
+    },
+    "mocha-teamcity-reporter": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/mocha-teamcity-reporter/-/mocha-teamcity-reporter-2.5.2.tgz",
+      "integrity": "sha512-zaJHKye3DSui3e9FdRKyt7fj9JxQxvlmSUu1Jow0VTyiYu6VsETY57Q4gwb2PWPZZgAugluamSnMCYC17hutGg==",
+      "dev": true,
+      "requires": {
+        "mocha": ">=3.5.0 < 6"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.15.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
+          "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
+          "dev": true
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+          "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "he": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+          "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
+          "dev": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
+        "mocha": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
+          "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
+          "dev": true,
+          "requires": {
+            "browser-stdout": "1.3.1",
+            "commander": "2.15.1",
+            "debug": "3.1.0",
+            "diff": "3.5.0",
+            "escape-string-regexp": "1.0.5",
+            "glob": "7.1.2",
+            "growl": "1.10.5",
+            "he": "1.1.1",
+            "minimatch": "3.0.4",
+            "mkdirp": "0.5.1",
+            "supports-color": "5.4.0"
+          }
+        },
+        "supports-color": {
+          "version": "5.4.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+          "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "mongodb": {
+      "version": "3.1.13",
+      "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.13.tgz",
+      "integrity": "sha512-sz2dhvBZQWf3LRNDhbd30KHVzdjZx9IKC0L+kSZ/gzYquCF5zPOgGqRz6sSCqYZtKP2ekB4nfLxhGtzGHnIKxA==",
+      "requires": {
+        "mongodb-core": "3.1.11",
+        "safe-buffer": "^5.1.2"
+      }
+    },
+    "mongodb-core": {
+      "version": "3.1.11",
+      "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.11.tgz",
+      "integrity": "sha512-rD2US2s5qk/ckbiiGFHeu+yKYDXdJ1G87F6CG3YdaZpzdOm5zpoAZd/EKbPmFO6cQZ+XVXBXBJ660sSI0gc6qg==",
+      "requires": {
+        "bson": "^1.1.0",
+        "require_optional": "^1.0.1",
+        "safe-buffer": "^5.1.2",
+        "saslprep": "^1.0.0"
+      }
+    },
+    "mongodb-download": {
+      "version": "2.2.7",
+      "resolved": "https://registry.npmjs.org/mongodb-download/-/mongodb-download-2.2.7.tgz",
+      "integrity": "sha512-39/eiEmCqig0gCR3tNbmbTk6rIpWzEGqcXT0BE645stlA+DY7WlrIWZGEG51BcI3MUdGzqVYFj+qLoRw+HsJSA==",
+      "requires": {
+        "debug": "^2.2.0",
+        "decompress": "^4.0.0",
+        "fs-extra": "^2.0.0",
+        "getos": "^2.7.0",
+        "md5-file": "3.1.1",
+        "request": "^2.79.0",
+        "request-promise": "^4.1.1",
+        "semver": "^5.6.0",
+        "yargs": "^3.26.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+          "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8="
+        },
+        "window-size": {
+          "version": "0.1.4",
+          "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz",
+          "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY="
+        },
+        "yargs": {
+          "version": "3.32.0",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz",
+          "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=",
+          "requires": {
+            "camelcase": "^2.0.1",
+            "cliui": "^3.0.3",
+            "decamelize": "^1.1.1",
+            "os-locale": "^1.4.0",
+            "string-width": "^1.0.1",
+            "window-size": "^0.1.4",
+            "y18n": "^3.2.0"
+          }
+        }
+      }
+    },
+    "mongodb-memory-server": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-5.1.0.tgz",
+      "integrity": "sha512-CN4fjt90fi6A3PSJIx6xYxIRqpxF1rJoz4BcJv6LmGyRAOAvCbr3SqRbfXJMS26wTDyHylXtW32PhdykDohogA==",
+      "dev": true,
+      "requires": {
+        "mongodb-memory-server-core": "5.1.0"
+      }
+    },
+    "mongodb-memory-server-core": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-5.1.0.tgz",
+      "integrity": "sha512-M/GWVvFze0geP2smoy0ns+Yz5l4BRP6Y66OKc+w9zlX422ydQEA04bLQjooPqoaBTrV90xuDj5E+zejn/YkPew==",
+      "dev": true,
+      "requires": {
+        "camelcase": "^5.3.1",
+        "debug": "^4.1.1",
+        "decompress": "^4.2.0",
+        "dedent": "^0.7.0",
+        "find-cache-dir": "^2.0.0",
+        "find-package-json": "^1.2.0",
+        "get-port": "^4.2.0",
+        "getos": "^3.1.1",
+        "https-proxy-agent": "^2.2.1",
+        "lockfile": "^1.0.4",
+        "md5-file": "^4.0.0",
+        "mkdirp": "^0.5.1",
+        "mongodb": ">=3.0.0",
+        "tmp": "^0.0.33",
+        "uuid": "^3.2.1"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "5.3.1",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+          "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+          "dev": true
+        },
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "getos": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/getos/-/getos-3.1.1.tgz",
+          "integrity": "sha512-oUP1rnEhAr97rkitiszGP9EgDVYnmchgFzfqRzSkgtfv7ai6tEi7Ko8GgjNXts7VLWEqrTWyhsOKLe5C5b/Zkg==",
+          "dev": true,
+          "requires": {
+            "async": "2.6.1"
+          }
+        },
+        "md5-file": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-4.0.0.tgz",
+          "integrity": "sha512-UC0qFwyAjn4YdPpKaDNw6gNxRf7Mcx7jC1UGCY4boCzgvU2Aoc1mOGzTtrjjLKhM5ivsnhoKpQVxKPp+1j1qwg==",
+          "dev": true
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+          "dev": true
+        }
+      }
+    },
+    "mongodb-prebuilt": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/mongodb-prebuilt/-/mongodb-prebuilt-6.5.0.tgz",
+      "integrity": "sha512-rwTWbV4w8uxYJAhq2tQd+lrAjOYsxo/eXJb5rvNCGEJZlddoThYOHlkfLQ4w7PagauQZN3XBEW55GhkPUadN6w==",
+      "requires": {
+        "debug": "^2.2.0",
+        "glob": "^7.1.1",
+        "mongodb-download": "^2.2.7",
+        "spawn-sync": "1.0.15",
+        "yargs": "^3.26.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+          "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8="
+        },
+        "window-size": {
+          "version": "0.1.4",
+          "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz",
+          "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY="
+        },
+        "yargs": {
+          "version": "3.32.0",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz",
+          "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=",
+          "requires": {
+            "camelcase": "^2.0.1",
+            "cliui": "^3.0.3",
+            "decamelize": "^1.1.1",
+            "os-locale": "^1.4.0",
+            "string-width": "^1.0.1",
+            "window-size": "^0.1.4",
+            "y18n": "^3.2.0"
+          }
+        }
+      }
+    },
+    "mongoose": {
+      "version": "5.5.4",
+      "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.5.4.tgz",
+      "integrity": "sha512-xzS7fJtXGjCOZozCtlyFS8graMub1L9knp37+1dJCDmWzOtXVHeLjV2XIC9tX0sE54cxeG5rHvSmIkLpeHjjmA==",
+      "requires": {
+        "async": "2.6.1",
+        "bson": "~1.1.1",
+        "kareem": "2.3.0",
+        "mongodb": "3.2.2",
+        "mongodb-core": "3.2.2",
+        "mongoose-legacy-pluralize": "1.0.2",
+        "mpath": "0.5.2",
+        "mquery": "3.2.0",
+        "ms": "2.1.1",
+        "regexp-clone": "0.0.1",
+        "safe-buffer": "5.1.2",
+        "sift": "7.0.1",
+        "sliced": "1.0.1"
+      },
+      "dependencies": {
+        "mongodb": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.2.2.tgz",
+          "integrity": "sha512-xQ6apOOV+w7VFApdaJpWhYhzartpjIDFQjG0AwgJkLh7dBs7PTsq4A3Bia2QWpDohmAzTBIdQVLMqqLy0mwt3Q==",
+          "requires": {
+            "mongodb-core": "3.2.2",
+            "safe-buffer": "^5.1.2"
+          }
+        },
+        "mongodb-core": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.2.2.tgz",
+          "integrity": "sha512-YRgC39MuzKL0uoGoRdTmV1e9m47NbMnYmuEx4IOkgWAGXPSEzRY7cwb3N0XMmrDMnD9vp7MysNyAriIIeGgIQg==",
+          "requires": {
+            "bson": "^1.1.1",
+            "require_optional": "^1.0.1",
+            "safe-buffer": "^5.1.2",
+            "saslprep": "^1.0.0"
+          }
+        },
+        "mpath": {
+          "version": "0.5.2",
+          "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.5.2.tgz",
+          "integrity": "sha512-NOeCoW6AYc3hLi30npe7uzbD9b4FQZKH40YKABUCCvaKKL5agj6YzvHoNx8jQpDMNPgIa5bvSZQbQpWBAVD0Kw=="
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+        }
+      }
+    },
+    "mongoose-legacy-pluralize": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz",
+      "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ=="
+    },
+    "morgan": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz",
+      "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==",
+      "requires": {
+        "basic-auth": "~2.0.0",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "on-finished": "~2.3.0",
+        "on-headers": "~1.0.1"
+      }
+    },
+    "mpath": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.5.1.tgz",
+      "integrity": "sha512-H8OVQ+QEz82sch4wbODFOz+3YQ61FYz/z3eJ5pIdbMEaUzDqA268Wd+Vt4Paw9TJfvDgVKaayC0gBzMIw2jhsg=="
+    },
+    "mquery": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.0.tgz",
+      "integrity": "sha512-qPJcdK/yqcbQiKoemAt62Y0BAc0fTEKo1IThodBD+O5meQRJT/2HSe5QpBNwaa4CjskoGrYWsEyjkqgiE0qjhg==",
+      "requires": {
+        "bluebird": "3.5.1",
+        "debug": "3.1.0",
+        "regexp-clone": "0.0.1",
+        "safe-buffer": "5.1.2",
+        "sliced": "1.0.1"
+      },
+      "dependencies": {
+        "bluebird": {
+          "version": "3.5.1",
+          "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
+          "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA=="
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+    },
+    "multistream": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/multistream/-/multistream-2.1.1.tgz",
+      "integrity": "sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ==",
+      "requires": {
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.5"
+      }
+    },
+    "mute-stream": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz",
+      "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA="
+    },
+    "nan": {
+      "version": "2.13.2",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
+      "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
+      "optional": true
+    },
+    "nanomatch": {
+      "version": "1.2.13",
+      "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+      "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "fragment-cache": "^0.2.1",
+        "is-windows": "^1.0.2",
+        "kind-of": "^6.0.2",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "arr-diff": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+          "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA="
+        },
+        "array-unique": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+          "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg="
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
+        }
+      }
+    },
+    "natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+      "dev": true
+    },
+    "nconf": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/nconf/-/nconf-0.10.0.tgz",
+      "integrity": "sha512-fKiXMQrpP7CYWJQzKkPPx9hPgmq+YLDyxcG9N8RpiE9FoCkCbzD0NyW0YhE3xn3Aupe7nnDeIx4PFzYehpHT9Q==",
+      "requires": {
+        "async": "^1.4.0",
+        "ini": "^1.3.0",
+        "secure-keys": "^1.0.0",
+        "yargs": "^3.19.0"
+      },
+      "dependencies": {
+        "async": {
+          "version": "1.5.2",
+          "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+          "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
+        },
+        "camelcase": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+          "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8="
+        },
+        "window-size": {
+          "version": "0.1.4",
+          "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz",
+          "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY="
+        },
+        "yargs": {
+          "version": "3.32.0",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz",
+          "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=",
+          "requires": {
+            "camelcase": "^2.0.1",
+            "cliui": "^3.0.3",
+            "decamelize": "^1.1.1",
+            "os-locale": "^1.4.0",
+            "string-width": "^1.0.1",
+            "window-size": "^0.1.4",
+            "y18n": "^3.2.0"
+          }
+        }
+      }
+    },
+    "negotiator": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
+      "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
+    },
+    "nice-try": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+      "dev": true
+    },
+    "node-environment-flags": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz",
+      "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==",
+      "dev": true,
+      "requires": {
+        "object.getownpropertydescriptors": "^2.0.3",
+        "semver": "^5.7.0"
+      }
+    },
+    "nodemailer": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.1.1.tgz",
+      "integrity": "sha512-/x5MRIh56VyuuhLfcz+DL2SlBARpZpgQIf2A4Ao4hMb69MHSgDIMPwYmFwesGT1lkRDZ0eBSoym5+JoIZ3N+cQ=="
+    },
+    "normalize-package-data": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+      "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+      "requires": {
+        "hosted-git-info": "^2.1.4",
+        "resolve": "^1.10.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      }
+    },
+    "normalize-path": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+      "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+      "requires": {
+        "remove-trailing-separator": "^1.0.1"
+      }
+    },
+    "npm-run-path": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+      "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+      "dev": true,
+      "requires": {
+        "path-key": "^2.0.0"
+      }
+    },
+    "number-is-nan": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
+    },
+    "oauth-sign": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+    },
+    "object-component": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+      "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
+    },
+    "object-copy": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+      "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+      "requires": {
+        "copy-descriptor": "^0.1.0",
+        "define-property": "^0.2.5",
+        "kind-of": "^3.0.3"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
+    "object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+      "dev": true
+    },
+    "object-visit": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+      "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+      "requires": {
+        "isobject": "^3.0.0"
+      },
+      "dependencies": {
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
+        }
+      }
+    },
+    "object.assign": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+      "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "function-bind": "^1.1.1",
+        "has-symbols": "^1.0.0",
+        "object-keys": "^1.0.11"
+      }
+    },
+    "object.getownpropertydescriptors": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz",
+      "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "es-abstract": "^1.5.1"
+      }
+    },
+    "object.omit": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz",
+      "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
+      "optional": true,
+      "requires": {
+        "for-own": "^0.1.4",
+        "is-extendable": "^0.1.1"
+      }
+    },
+    "object.pick": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+      "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+      "requires": {
+        "isobject": "^3.0.1"
+      },
+      "dependencies": {
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
+        }
+      }
+    },
+    "on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+      "requires": {
+        "ee-first": "1.1.1"
+      }
+    },
+    "on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "onetime": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
+      "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k="
+    },
+    "optionator": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
+      "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
+      "requires": {
+        "deep-is": "~0.1.3",
+        "fast-levenshtein": "~2.0.4",
+        "levn": "~0.3.0",
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2",
+        "wordwrap": "~1.0.0"
+      }
+    },
+    "os-homedir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
+    },
+    "os-locale": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+      "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
+      "requires": {
+        "lcid": "^1.0.0"
+      }
+    },
+    "os-shim": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz",
+      "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc="
+    },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
+    },
+    "output-file-sync": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz",
+      "integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY=",
+      "requires": {
+        "graceful-fs": "^4.1.4",
+        "mkdirp": "^0.5.1",
+        "object-assign": "^4.1.0"
+      }
+    },
+    "p-defer": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
+      "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=",
+      "dev": true
+    },
+    "p-finally": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+      "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
+      "dev": true
+    },
+    "p-is-promise": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz",
+      "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg=="
+    },
+    "p-limit": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz",
+      "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==",
+      "dev": true,
+      "requires": {
+        "p-try": "^2.0.0"
+      }
+    },
+    "p-locate": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+      "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+      "dev": true,
+      "requires": {
+        "p-limit": "^2.0.0"
+      }
+    },
+    "p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "dev": true
+    },
+    "pako": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz",
+      "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw=="
+    },
+    "parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "requires": {
+        "callsites": "^3.0.0"
+      }
+    },
+    "parse-glob": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
+      "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=",
+      "optional": true,
+      "requires": {
+        "glob-base": "^0.3.0",
+        "is-dotfile": "^1.0.0",
+        "is-extglob": "^1.0.0",
+        "is-glob": "^2.0.0"
+      }
+    },
+    "parse-json": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+      "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+      "requires": {
+        "error-ex": "^1.2.0"
+      }
+    },
+    "parseqs": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+      "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseuri": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+      "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+    },
+    "pascalcase": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+      "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ="
+    },
+    "passport": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz",
+      "integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=",
+      "requires": {
+        "passport-strategy": "1.x.x",
+        "pause": "0.0.1"
+      }
+    },
+    "passport-local": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
+      "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=",
+      "requires": {
+        "passport-strategy": "1.x.x"
+      }
+    },
+    "passport-local-mongoose": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/passport-local-mongoose/-/passport-local-mongoose-5.0.1.tgz",
+      "integrity": "sha512-VUY5DgBdpjt1tjunJJ1EXV5b2nhMDkXJuhTjyiK660IgIp7kONMyWEe9tGHf8I9tZudXuTF+47JNQLIzU+Hjbw==",
+      "requires": {
+        "debug": "^3.1.0",
+        "generaterr": "^1.5.0",
+        "passport-local": "^1.0.0",
+        "scmp": "^2.0.0",
+        "semver": "^5.5.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+        }
+      }
+    },
+    "passport-strategy": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
+      "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ="
+    },
+    "passport.socketio": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/passport.socketio/-/passport.socketio-3.7.0.tgz",
+      "integrity": "sha1-LuX6/paV1CgcjN3T/pdezRjmcm4=",
+      "requires": {
+        "xtend": "^4.0.0"
+      }
+    },
+    "path-dirname": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+      "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA="
+    },
+    "path-exists": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+      "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+      "requires": {
+        "pinkie-promise": "^2.0.0"
+      }
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+    },
+    "path-is-inside": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+      "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+      "dev": true
+    },
+    "path-key": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+      "dev": true
+    },
+    "path-parse": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
+    },
+    "path-to-regexp": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+    },
+    "path-type": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+      "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "pify": "^2.0.0",
+        "pinkie-promise": "^2.0.0"
+      }
+    },
+    "pathval": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
+      "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
+      "dev": true
+    },
+    "pause": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
+      "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
+    },
+    "pend": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+      "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
+    },
+    "performance-now": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
+    },
+    "pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
+    },
+    "pinkie": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+      "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA="
+    },
+    "pinkie-promise": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+      "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+      "requires": {
+        "pinkie": "^2.0.0"
+      }
+    },
+    "pkg": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/pkg/-/pkg-4.3.7.tgz",
+      "integrity": "sha512-/BvtFft1nKKtnTuOm/0es0sk1cOs7ZtWgJpqdtszJ4348jYJ8owVyCB/iuGhI3YJFX/ZFIv4Rmra9ETUgpnnfA==",
+      "requires": {
+        "@babel/parser": "7.2.3",
+        "babel-runtime": "6.26.0",
+        "chalk": "2.4.2",
+        "escodegen": "1.11.0",
+        "fs-extra": "7.0.1",
+        "globby": "8.0.2",
+        "into-stream": "4.0.0",
+        "minimist": "1.2.0",
+        "multistream": "2.1.1",
+        "pkg-fetch": "2.5.7",
+        "progress": "2.0.3",
+        "resolve": "1.6.0",
+        "stream-meter": "1.0.4"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "requires": {
+            "color-convert": "^1.9.0"
+          }
+        },
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "fs-extra": {
+          "version": "7.0.1",
+          "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
+          "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
+          "requires": {
+            "graceful-fs": "^4.1.2",
+            "jsonfile": "^4.0.0",
+            "universalify": "^0.1.0"
+          }
+        },
+        "jsonfile": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+          "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+          "requires": {
+            "graceful-fs": "^4.1.6"
+          }
+        },
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+        },
+        "resolve": {
+          "version": "1.6.0",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.6.0.tgz",
+          "integrity": "sha512-mw7JQNu5ExIkcw4LPih0owX/TZXjD/ZUF/ZQ/pDnkw3ZKhDcZZw5klmBlj6gVMwjQ3Pz5Jgu7F3d0jcDVuEWdw==",
+          "requires": {
+            "path-parse": "^1.0.5"
+          }
+        },
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "pkg-dir": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
+      "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==",
+      "dev": true,
+      "requires": {
+        "find-up": "^3.0.0"
+      },
+      "dependencies": {
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        }
+      }
+    },
+    "pkg-fetch": {
+      "version": "2.5.7",
+      "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-2.5.7.tgz",
+      "integrity": "sha512-fm9aVV3ZRdFYTyFYcSHuKMuxPCVQ0MD9tbVxbvQzFTg1gwvV0KqWrFoj5enVVha94yP83I50XEBa90X8L9fE8w==",
+      "requires": {
+        "babel-runtime": "~6.26.0",
+        "byline": "~5.0.0",
+        "chalk": "~2.4.1",
+        "expand-template": "~1.1.1",
+        "fs-extra": "~6.0.1",
+        "in-publish": "~2.0.0",
+        "minimist": "~1.2.0",
+        "progress": "~2.0.0",
+        "request": "~2.88.0",
+        "request-progress": "~3.0.0",
+        "semver": "~5.6.0",
+        "unique-temp-dir": "~1.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "requires": {
+            "color-convert": "^1.9.0"
+          }
+        },
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "fs-extra": {
+          "version": "6.0.1",
+          "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz",
+          "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==",
+          "requires": {
+            "graceful-fs": "^4.1.2",
+            "jsonfile": "^4.0.0",
+            "universalify": "^0.1.0"
+          }
+        },
+        "jsonfile": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+          "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+          "requires": {
+            "graceful-fs": "^4.1.6"
+          }
+        },
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+        },
+        "semver": {
+          "version": "5.6.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
+          "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
+        },
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "posix-character-classes": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+      "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs="
+    },
+    "prelude-ls": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+      "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ="
+    },
+    "preserve": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz",
+      "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
+      "optional": true
+    },
+    "private": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
+      "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg=="
+    },
+    "process-nextick-args": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+      "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
+    },
+    "progress": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+      "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="
+    },
+    "proxy-addr": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
+      "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==",
+      "requires": {
+        "forwarded": "~0.1.2",
+        "ipaddr.js": "1.9.0"
+      }
+    },
+    "psl": {
+      "version": "1.1.31",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
+      "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw=="
+    },
+    "pump": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+      "dev": true,
+      "requires": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
+    },
+    "qs": {
+      "version": "6.7.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+      "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
+    },
+    "random-bytes": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+      "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
+    },
+    "randomatic": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz",
+      "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==",
+      "optional": true,
+      "requires": {
+        "is-number": "^4.0.0",
+        "kind-of": "^6.0.0",
+        "math-random": "^1.0.1"
+      },
+      "dependencies": {
+        "is-number": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz",
+          "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==",
+          "optional": true
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "optional": true
+        }
+      }
+    },
+    "range-parser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+      "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
+    },
+    "raw-body": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+      "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
+      "requires": {
+        "bytes": "3.1.0",
+        "http-errors": "1.7.2",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
+      }
+    },
+    "read-pkg": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+      "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+      "requires": {
+        "load-json-file": "^1.0.0",
+        "normalize-package-data": "^2.3.2",
+        "path-type": "^1.0.0"
+      }
+    },
+    "read-pkg-up": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+      "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+      "requires": {
+        "find-up": "^1.0.0",
+        "read-pkg": "^1.0.0"
+      }
+    },
+    "readable-stream": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+      "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+      "requires": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "readdirp": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+      "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+      "optional": true,
+      "requires": {
+        "graceful-fs": "^4.1.11",
+        "micromatch": "^3.1.10",
+        "readable-stream": "^2.0.2"
+      },
+      "dependencies": {
+        "arr-diff": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+          "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+          "optional": true
+        },
+        "array-unique": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+          "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg="
+        },
+        "braces": {
+          "version": "2.3.2",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+          "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+          "optional": true,
+          "requires": {
+            "arr-flatten": "^1.1.0",
+            "array-unique": "^0.3.2",
+            "extend-shallow": "^2.0.1",
+            "fill-range": "^4.0.0",
+            "isobject": "^3.0.1",
+            "repeat-element": "^1.1.2",
+            "snapdragon": "^0.8.1",
+            "snapdragon-node": "^2.0.1",
+            "split-string": "^3.0.2",
+            "to-regex": "^3.0.1"
+          },
+          "dependencies": {
+            "extend-shallow": {
+              "version": "2.0.1",
+              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+              "optional": true,
+              "requires": {
+                "is-extendable": "^0.1.0"
+              }
+            }
+          }
+        },
+        "expand-brackets": {
+          "version": "2.1.4",
+          "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+          "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+          "optional": true,
+          "requires": {
+            "debug": "^2.3.3",
+            "define-property": "^0.2.5",
+            "extend-shallow": "^2.0.1",
+            "posix-character-classes": "^0.1.0",
+            "regex-not": "^1.0.0",
+            "snapdragon": "^0.8.1",
+            "to-regex": "^3.0.1"
+          },
+          "dependencies": {
+            "define-property": {
+              "version": "0.2.5",
+              "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+              "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+              "optional": true,
+              "requires": {
+                "is-descriptor": "^0.1.0"
+              }
+            },
+            "extend-shallow": {
+              "version": "2.0.1",
+              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+              "optional": true,
+              "requires": {
+                "is-extendable": "^0.1.0"
+              }
+            },
+            "is-accessor-descriptor": {
+              "version": "0.1.6",
+              "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+              "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+              "optional": true,
+              "requires": {
+                "kind-of": "^3.0.2"
+              },
+              "dependencies": {
+                "kind-of": {
+                  "version": "3.2.2",
+                  "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+                  "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+                  "optional": true,
+                  "requires": {
+                    "is-buffer": "^1.1.5"
+                  }
+                }
+              }
+            },
+            "is-data-descriptor": {
+              "version": "0.1.4",
+              "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+              "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+              "optional": true,
+              "requires": {
+                "kind-of": "^3.0.2"
+              },
+              "dependencies": {
+                "kind-of": {
+                  "version": "3.2.2",
+                  "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+                  "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+                  "optional": true,
+                  "requires": {
+                    "is-buffer": "^1.1.5"
+                  }
+                }
+              }
+            },
+            "is-descriptor": {
+              "version": "0.1.6",
+              "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+              "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+              "optional": true,
+              "requires": {
+                "is-accessor-descriptor": "^0.1.6",
+                "is-data-descriptor": "^0.1.4",
+                "kind-of": "^5.0.0"
+              }
+            },
+            "kind-of": {
+              "version": "5.1.0",
+              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+              "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+              "optional": true
+            }
+          }
+        },
+        "extglob": {
+          "version": "2.0.4",
+          "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+          "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+          "optional": true,
+          "requires": {
+            "array-unique": "^0.3.2",
+            "define-property": "^1.0.0",
+            "expand-brackets": "^2.1.4",
+            "extend-shallow": "^2.0.1",
+            "fragment-cache": "^0.2.1",
+            "regex-not": "^1.0.0",
+            "snapdragon": "^0.8.1",
+            "to-regex": "^3.0.1"
+          },
+          "dependencies": {
+            "define-property": {
+              "version": "1.0.0",
+              "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+              "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+              "optional": true,
+              "requires": {
+                "is-descriptor": "^1.0.0"
+              }
+            },
+            "extend-shallow": {
+              "version": "2.0.1",
+              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+              "optional": true,
+              "requires": {
+                "is-extendable": "^0.1.0"
+              }
+            }
+          }
+        },
+        "fill-range": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+          "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+          "optional": true,
+          "requires": {
+            "extend-shallow": "^2.0.1",
+            "is-number": "^3.0.0",
+            "repeat-string": "^1.6.1",
+            "to-regex-range": "^2.1.0"
+          },
+          "dependencies": {
+            "extend-shallow": {
+              "version": "2.0.1",
+              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+              "optional": true,
+              "requires": {
+                "is-extendable": "^0.1.0"
+              }
+            }
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "optional": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "optional": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "optional": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        },
+        "is-number": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+          "optional": true,
+          "requires": {
+            "kind-of": "^3.0.2"
+          },
+          "dependencies": {
+            "kind-of": {
+              "version": "3.2.2",
+              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+              "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+              "optional": true,
+              "requires": {
+                "is-buffer": "^1.1.5"
+              }
+            }
+          }
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "optional": true
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
+        },
+        "micromatch": {
+          "version": "3.1.10",
+          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+          "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+          "optional": true,
+          "requires": {
+            "arr-diff": "^4.0.0",
+            "array-unique": "^0.3.2",
+            "braces": "^2.3.1",
+            "define-property": "^2.0.2",
+            "extend-shallow": "^3.0.2",
+            "extglob": "^2.0.4",
+            "fragment-cache": "^0.2.1",
+            "kind-of": "^6.0.2",
+            "nanomatch": "^1.2.9",
+            "object.pick": "^1.3.0",
+            "regex-not": "^1.0.0",
+            "snapdragon": "^0.8.1",
+            "to-regex": "^3.0.2"
+          }
+        }
+      }
+    },
+    "readline2": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz",
+      "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=",
+      "requires": {
+        "code-point-at": "^1.0.0",
+        "is-fullwidth-code-point": "^1.0.0",
+        "mute-stream": "0.0.5"
+      }
+    },
+    "regenerate": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
+      "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg=="
+    },
+    "regenerator-runtime": {
+      "version": "0.11.1",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+      "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
+    },
+    "regenerator-transform": {
+      "version": "0.10.1",
+      "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz",
+      "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==",
+      "requires": {
+        "babel-runtime": "^6.18.0",
+        "babel-types": "^6.19.0",
+        "private": "^0.1.6"
+      }
+    },
+    "regex-cache": {
+      "version": "0.4.4",
+      "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
+      "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==",
+      "optional": true,
+      "requires": {
+        "is-equal-shallow": "^0.1.3"
+      }
+    },
+    "regex-not": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+      "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+      "requires": {
+        "extend-shallow": "^3.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "regexp-clone": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz",
+      "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk="
+    },
+    "regexpp": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
+      "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+      "dev": true
+    },
+    "regexpu-core": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz",
+      "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=",
+      "requires": {
+        "regenerate": "^1.2.1",
+        "regjsgen": "^0.2.0",
+        "regjsparser": "^0.1.4"
+      }
+    },
+    "regjsgen": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
+      "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc="
+    },
+    "regjsparser": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
+      "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
+      "requires": {
+        "jsesc": "~0.5.0"
+      },
+      "dependencies": {
+        "jsesc": {
+          "version": "0.5.0",
+          "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+          "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0="
+        }
+      }
+    },
+    "remove-trailing-separator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+      "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8="
+    },
+    "repeat-element": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
+      "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g=="
+    },
+    "repeat-string": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+      "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
+    },
+    "repeating": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+      "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+      "requires": {
+        "is-finite": "^1.0.0"
+      }
+    },
+    "request": {
+      "version": "2.88.0",
+      "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
+      "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+      "requires": {
+        "aws-sign2": "~0.7.0",
+        "aws4": "^1.8.0",
+        "caseless": "~0.12.0",
+        "combined-stream": "~1.0.6",
+        "extend": "~3.0.2",
+        "forever-agent": "~0.6.1",
+        "form-data": "~2.3.2",
+        "har-validator": "~5.1.0",
+        "http-signature": "~1.2.0",
+        "is-typedarray": "~1.0.0",
+        "isstream": "~0.1.2",
+        "json-stringify-safe": "~5.0.1",
+        "mime-types": "~2.1.19",
+        "oauth-sign": "~0.9.0",
+        "performance-now": "^2.1.0",
+        "qs": "~6.5.2",
+        "safe-buffer": "^5.1.2",
+        "tough-cookie": "~2.4.3",
+        "tunnel-agent": "^0.6.0",
+        "uuid": "^3.3.2"
+      },
+      "dependencies": {
+        "qs": {
+          "version": "6.5.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+          "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+        }
+      }
+    },
+    "request-progress": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
+      "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=",
+      "requires": {
+        "throttleit": "^1.0.0"
+      }
+    },
+    "request-promise": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.4.tgz",
+      "integrity": "sha512-8wgMrvE546PzbR5WbYxUQogUnUDfM0S7QIFZMID+J73vdFARkFy+HElj4T+MWYhpXwlLp0EQ8Zoj8xUA0he4Vg==",
+      "requires": {
+        "bluebird": "^3.5.0",
+        "request-promise-core": "1.1.2",
+        "stealthy-require": "^1.1.1",
+        "tough-cookie": "^2.3.3"
+      }
+    },
+    "request-promise-core": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz",
+      "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==",
+      "requires": {
+        "lodash": "^4.17.11"
+      }
+    },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
+    },
+    "require-main-filename": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+      "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE="
+    },
+    "require_optional": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
+      "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
+      "requires": {
+        "resolve-from": "^2.0.0",
+        "semver": "^5.1.0"
+      }
+    },
+    "resolve": {
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz",
+      "integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==",
+      "requires": {
+        "path-parse": "^1.0.6"
+      }
+    },
+    "resolve-from": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
+      "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
+    },
+    "resolve-url": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+      "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo="
+    },
+    "restore-cursor": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz",
+      "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=",
+      "requires": {
+        "exit-hook": "^1.0.0",
+        "onetime": "^1.0.0"
+      }
+    },
+    "ret": {
+      "version": "0.1.15",
+      "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+      "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
+    },
+    "rimraf": {
+      "version": "2.6.3",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+      "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.3"
+      }
+    },
+    "run-async": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz",
+      "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=",
+      "requires": {
+        "once": "^1.3.0"
+      }
+    },
+    "rx-lite": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz",
+      "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI="
+    },
+    "rxjs": {
+      "version": "6.5.1",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.1.tgz",
+      "integrity": "sha512-y0j31WJc83wPu31vS1VlAFW5JGrnGC+j+TtGAa1fRQphy48+fDYiDmX8tjGloToEsMkxnouOg/1IzXGKkJnZMg==",
+      "dev": true,
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
+    "safe-regex": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+      "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+      "requires": {
+        "ret": "~0.1.10"
+      }
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "saslprep": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.2.tgz",
+      "integrity": "sha512-4cDsYuAjXssUSjxHKRe4DTZC0agDwsCqcMqtJAQPzC74nJ7LfAJflAtC1Zed5hMzEQKj82d3tuzqdGNRsLJ4Gw==",
+      "optional": true,
+      "requires": {
+        "sparse-bitfield": "^3.0.3"
+      }
+    },
+    "scmp": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.0.0.tgz",
+      "integrity": "sha1-JHEQ7yLM+JexOj8KvdtSeCOTzWo="
+    },
+    "secure-keys": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/secure-keys/-/secure-keys-1.0.0.tgz",
+      "integrity": "sha1-8MgtmKOxOah3aogIBQuCRDEIf8o="
+    },
+    "seek-bzip": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz",
+      "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=",
+      "requires": {
+        "commander": "~2.8.1"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.8.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz",
+          "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=",
+          "requires": {
+            "graceful-readlink": ">= 1.0.0"
+          }
+        }
+      }
+    },
+    "semver": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
+      "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
+    },
+    "send": {
+      "version": "0.16.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
+      "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+      "requires": {
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "destroy": "~1.0.4",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "~1.6.2",
+        "mime": "1.4.1",
+        "ms": "2.0.0",
+        "on-finished": "~2.3.0",
+        "range-parser": "~1.2.0",
+        "statuses": "~1.4.0"
+      },
+      "dependencies": {
+        "http-errors": {
+          "version": "1.6.3",
+          "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+          "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+          "requires": {
+            "depd": "~1.1.2",
+            "inherits": "2.0.3",
+            "setprototypeof": "1.1.0",
+            "statuses": ">= 1.4.0 < 2"
+          }
+        },
+        "setprototypeof": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+          "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
+        },
+        "statuses": {
+          "version": "1.4.0",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+          "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
+        }
+      }
+    },
+    "serve-static": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
+      "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+      "requires": {
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.2",
+        "send": "0.16.2"
+      }
+    },
+    "set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
+    },
+    "set-immediate-shim": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
+      "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
+    },
+    "set-value": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
+      "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+      "requires": {
+        "extend-shallow": "^2.0.1",
+        "is-extendable": "^0.1.1",
+        "is-plain-object": "^2.0.3",
+        "split-string": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "setprototypeof": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+      "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
+    },
+    "shebang-command": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+      "dev": true,
+      "requires": {
+        "shebang-regex": "^1.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+      "dev": true
+    },
+    "sift": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz",
+      "integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g=="
+    },
+    "sigmund": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
+      "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA="
+    },
+    "signal-exit": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+      "dev": true
+    },
+    "simple-glob": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/simple-glob/-/simple-glob-0.1.1.tgz",
+      "integrity": "sha1-KCv6AS1yBmQ99h00xrueTOP9dxQ=",
+      "requires": {
+        "glob": "~3.2.8",
+        "lodash": "~2.4.1",
+        "minimatch": "~0.2.14"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "3.2.11",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz",
+          "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=",
+          "requires": {
+            "inherits": "2",
+            "minimatch": "0.3"
+          },
+          "dependencies": {
+            "minimatch": {
+              "version": "0.3.0",
+              "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz",
+              "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=",
+              "requires": {
+                "lru-cache": "2",
+                "sigmund": "~1.0.0"
+              }
+            }
+          }
+        },
+        "lodash": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz",
+          "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4="
+        }
+      }
+    },
+    "slash": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
+      "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU="
+    },
+    "slice-ansi": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
+      "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^3.2.0",
+        "astral-regex": "^1.0.0",
+        "is-fullwidth-code-point": "^2.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^1.9.0"
+          }
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        }
+      }
+    },
+    "sliced": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
+      "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
+    },
+    "snapdragon": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+      "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+      "requires": {
+        "base": "^0.11.1",
+        "debug": "^2.2.0",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "map-cache": "^0.2.2",
+        "source-map": "^0.5.6",
+        "source-map-resolve": "^0.5.0",
+        "use": "^3.1.0"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "snapdragon-node": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+      "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+      "requires": {
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.0",
+        "snapdragon-util": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
+        }
+      }
+    },
+    "snapdragon-util": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+      "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+      "requires": {
+        "kind-of": "^3.2.0"
+      }
+    },
+    "socket.io": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz",
+      "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==",
+      "requires": {
+        "debug": "~3.1.0",
+        "engine.io": "~3.2.0",
+        "has-binary2": "~1.0.2",
+        "socket.io-adapter": "~1.1.0",
+        "socket.io-client": "2.1.1",
+        "socket.io-parser": "~3.2.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "socket.io-adapter": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
+      "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs="
+    },
+    "socket.io-client": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz",
+      "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==",
+      "requires": {
+        "backo2": "1.0.2",
+        "base64-arraybuffer": "0.1.5",
+        "component-bind": "1.0.0",
+        "component-emitter": "1.2.1",
+        "debug": "~3.1.0",
+        "engine.io-client": "~3.2.0",
+        "has-binary2": "~1.0.2",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "object-component": "0.0.3",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "socket.io-parser": "~3.2.0",
+        "to-array": "0.1.4"
+      },
+      "dependencies": {
+        "component-emitter": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+          "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "socket.io-parser": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz",
+      "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==",
+      "requires": {
+        "component-emitter": "1.2.1",
+        "debug": "~3.1.0",
+        "isarray": "2.0.1"
+      },
+      "dependencies": {
+        "component-emitter": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+          "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+        }
+      }
+    },
+    "source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
+    },
+    "source-map-resolve": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
+      "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==",
+      "requires": {
+        "atob": "^2.1.1",
+        "decode-uri-component": "^0.2.0",
+        "resolve-url": "^0.2.1",
+        "source-map-url": "^0.4.0",
+        "urix": "^0.1.0"
+      }
+    },
+    "source-map-support": {
+      "version": "0.4.18",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
+      "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
+      "requires": {
+        "source-map": "^0.5.6"
+      }
+    },
+    "source-map-url": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
+      "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM="
+    },
+    "sparse-bitfield": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+      "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
+      "optional": true,
+      "requires": {
+        "memory-pager": "^1.0.2"
+      }
+    },
+    "spawn-sync": {
+      "version": "1.0.15",
+      "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz",
+      "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=",
+      "requires": {
+        "concat-stream": "^1.4.7",
+        "os-shim": "^0.1.2"
+      }
+    },
+    "spdx-correct": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
+      "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
+      "requires": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-exceptions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
+      "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA=="
+    },
+    "spdx-expression-parse": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+      "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+      "requires": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-license-ids": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz",
+      "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA=="
+    },
+    "split-string": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+      "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+      "requires": {
+        "extend-shallow": "^3.0.0"
+      }
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+      "dev": true
+    },
+    "sshpk": {
+      "version": "1.16.1",
+      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
+      "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
+      "requires": {
+        "asn1": "~0.2.3",
+        "assert-plus": "^1.0.0",
+        "bcrypt-pbkdf": "^1.0.0",
+        "dashdash": "^1.12.0",
+        "ecc-jsbn": "~0.1.1",
+        "getpass": "^0.1.1",
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.0.2",
+        "tweetnacl": "~0.14.0"
+      }
+    },
+    "standard-error": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/standard-error/-/standard-error-1.1.0.tgz",
+      "integrity": "sha1-I+UWj6HAggGJ5YEnAaeQWFENDTQ="
+    },
+    "static-extend": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+      "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+      "requires": {
+        "define-property": "^0.2.5",
+        "object-copy": "^0.1.0"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
+    "statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+    },
+    "stealthy-require": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
+      "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
+    },
+    "stream-meter": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz",
+      "integrity": "sha1-Uq+Vql6nYKJJFxZwTb/5D3Ov3R0=",
+      "requires": {
+        "readable-stream": "^2.1.4"
+      }
+    },
+    "string-width": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+      "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+      "requires": {
+        "code-point-at": "^1.0.0",
+        "is-fullwidth-code-point": "^1.0.0",
+        "strip-ansi": "^3.0.0"
+      }
+    },
+    "string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "requires": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "strip-ansi": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+      "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+      "requires": {
+        "ansi-regex": "^2.0.0"
+      }
+    },
+    "strip-bom": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+      "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+      "requires": {
+        "is-utf8": "^0.2.0"
+      }
+    },
+    "strip-dirs": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz",
+      "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==",
+      "requires": {
+        "is-natural-number": "^4.0.1"
+      }
+    },
+    "strip-eof": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+      "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
+      "dev": true
+    },
+    "strip-json-comments": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+      "dev": true
+    },
+    "superagent": {
+      "version": "3.8.3",
+      "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
+      "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==",
+      "dev": true,
+      "requires": {
+        "component-emitter": "^1.2.0",
+        "cookiejar": "^2.1.0",
+        "debug": "^3.1.0",
+        "extend": "^3.0.0",
+        "form-data": "^2.3.1",
+        "formidable": "^1.2.0",
+        "methods": "^1.1.1",
+        "mime": "^1.4.1",
+        "qs": "^6.5.1",
+        "readable-stream": "^2.3.5"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+          "dev": true
+        }
+      }
+    },
+    "supertest": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.0.0.tgz",
+      "integrity": "sha1-jUu2j9GDDuBwM7HFpamkAhyWUpY=",
+      "dev": true,
+      "requires": {
+        "methods": "~1.1.2",
+        "superagent": "^3.0.0"
+      }
+    },
+    "supports-color": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+      "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
+    },
+    "table": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/table/-/table-5.2.3.tgz",
+      "integrity": "sha512-N2RsDAMvDLvYwFcwbPyF3VmVSSkuF+G1e+8inhBLtHpvwXGw4QRPEZhihQNeEN0i1up6/f6ObCJXNdlRG3YVyQ==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.9.1",
+        "lodash": "^4.17.11",
+        "slice-ansi": "^2.1.0",
+        "string-width": "^3.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        }
+      }
+    },
+    "tar-stream": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz",
+      "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==",
+      "requires": {
+        "bl": "^1.0.0",
+        "buffer-alloc": "^1.2.0",
+        "end-of-stream": "^1.0.0",
+        "fs-constants": "^1.0.0",
+        "readable-stream": "^2.3.0",
+        "to-buffer": "^1.1.1",
+        "xtend": "^4.0.0"
+      }
+    },
+    "text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+      "dev": true
+    },
+    "throttleit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
+      "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw="
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
+    },
+    "tmp": {
+      "version": "0.0.33",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+      "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+      "dev": true,
+      "requires": {
+        "os-tmpdir": "~1.0.2"
+      }
+    },
+    "to-array": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+      "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
+    },
+    "to-buffer": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
+      "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg=="
+    },
+    "to-fast-properties": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
+      "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc="
+    },
+    "to-object-path": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+      "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+      "requires": {
+        "kind-of": "^3.0.2"
+      }
+    },
+    "to-regex": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+      "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+      "requires": {
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "regex-not": "^1.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "to-regex-range": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+      "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+      "requires": {
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1"
+      },
+      "dependencies": {
+        "is-number": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+          "requires": {
+            "kind-of": "^3.0.2"
+          }
+        }
+      }
+    },
+    "toidentifier": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+      "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
+    },
+    "tough-cookie": {
+      "version": "2.4.3",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+      "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+      "requires": {
+        "psl": "^1.1.24",
+        "punycode": "^1.4.1"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
+        }
+      }
+    },
+    "trim-right": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
+      "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM="
+    },
+    "ts-node": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-6.2.0.tgz",
+      "integrity": "sha512-ZNT+OEGfUNVMGkpIaDJJ44Zq3Yr0bkU/ugN1PHbU+/01Z7UV1fsELRiTx1KuQNvQ1A3pGh3y25iYF6jXgxV21A==",
+      "requires": {
+        "arrify": "^1.0.0",
+        "buffer-from": "^1.1.0",
+        "diff": "^3.1.0",
+        "make-error": "^1.1.1",
+        "minimist": "^1.2.0",
+        "mkdirp": "^0.5.1",
+        "source-map-support": "^0.5.6",
+        "yn": "^2.0.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+        },
+        "source-map-support": {
+          "version": "0.5.12",
+          "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz",
+          "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==",
+          "requires": {
+            "buffer-from": "^1.0.0",
+            "source-map": "^0.6.0"
+          }
+        }
+      }
+    },
+    "tslib": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
+      "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
+      "dev": true
+    },
+    "tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
+    },
+    "type-check": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+      "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+      "requires": {
+        "prelude-ls": "~1.1.2"
+      }
+    },
+    "type-detect": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+      "dev": true
+    },
+    "type-is": {
+      "version": "1.6.17",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.17.tgz",
+      "integrity": "sha512-jYZzkOoAPVyQ9vlZ4xEJ4BBbHC4a7hbY1xqyCPe6AiQVVqfbZEulJm0VpqK4B+096O1VQi0l6OBGH210ejx/bA==",
+      "requires": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      }
+    },
+    "typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+    },
+    "uid-safe": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+      "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+      "requires": {
+        "random-bytes": "~1.0.0"
+      }
+    },
+    "uid2": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz",
+      "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I="
+    },
+    "ultron": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
+      "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
+    },
+    "unbzip2-stream": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz",
+      "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==",
+      "requires": {
+        "buffer": "^5.2.1",
+        "through": "^2.3.8"
+      }
+    },
+    "union-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
+      "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+      "requires": {
+        "arr-union": "^3.1.0",
+        "get-value": "^2.0.6",
+        "is-extendable": "^0.1.1",
+        "set-value": "^0.4.3"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "set-value": {
+          "version": "0.4.3",
+          "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
+          "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
+          "requires": {
+            "extend-shallow": "^2.0.1",
+            "is-extendable": "^0.1.1",
+            "is-plain-object": "^2.0.1",
+            "to-object-path": "^0.3.0"
+          }
+        }
+      }
+    },
+    "unique-temp-dir": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz",
+      "integrity": "sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U=",
+      "requires": {
+        "mkdirp": "^0.5.1",
+        "os-tmpdir": "^1.0.1",
+        "uid2": "0.0.3"
+      }
+    },
+    "universalify": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+      "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
+    },
+    "unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+    },
+    "unset-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+      "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+      "requires": {
+        "has-value": "^0.3.1",
+        "isobject": "^3.0.0"
+      },
+      "dependencies": {
+        "has-value": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+          "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+          "requires": {
+            "get-value": "^2.0.3",
+            "has-values": "^0.1.4",
+            "isobject": "^2.0.0"
+          },
+          "dependencies": {
+            "isobject": {
+              "version": "2.1.0",
+              "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+              "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+              "requires": {
+                "isarray": "1.0.0"
+              }
+            }
+          }
+        },
+        "has-values": {
+          "version": "0.1.4",
+          "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+          "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E="
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
+        }
+      }
+    },
+    "uri-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+      "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "urix": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+      "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
+    },
+    "use": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+      "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
+    },
+    "user-home": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz",
+      "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA="
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+    },
+    "utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+    },
+    "uuid": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+      "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
+    },
+    "v8flags": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz",
+      "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=",
+      "requires": {
+        "user-home": "^1.1.1"
+      }
+    },
+    "validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "requires": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+    },
+    "verror": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "^1.2.0"
+      }
+    },
+    "which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "dev": true,
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "which-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
+      "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8="
+    },
+    "wide-align": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+      "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+      "dev": true,
+      "requires": {
+        "string-width": "^1.0.2 || 2"
+      }
+    },
+    "window-size": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz",
+      "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU="
+    },
+    "wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
+    },
+    "wrap-ansi": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+      "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+      "requires": {
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.1"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+    },
+    "write": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz",
+      "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==",
+      "dev": true,
+      "requires": {
+        "mkdirp": "^0.5.1"
+      }
+    },
+    "ws": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
+      "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
+      "requires": {
+        "async-limiter": "~1.0.0",
+        "safe-buffer": "~5.1.0",
+        "ultron": "~1.1.0"
+      }
+    },
+    "xmlhttprequest-ssl": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
+      "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
+    },
+    "xtend": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+      "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
+    },
+    "y18n": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+      "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
+    },
+    "yargs": {
+      "version": "4.8.1",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz",
+      "integrity": "sha1-wMQpJMpKqmsObaFznfshZDn53cA=",
+      "requires": {
+        "cliui": "^3.2.0",
+        "decamelize": "^1.1.1",
+        "get-caller-file": "^1.0.1",
+        "lodash.assign": "^4.0.3",
+        "os-locale": "^1.4.0",
+        "read-pkg-up": "^1.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^1.0.1",
+        "set-blocking": "^2.0.0",
+        "string-width": "^1.0.1",
+        "which-module": "^1.0.0",
+        "window-size": "^0.2.0",
+        "y18n": "^3.2.1",
+        "yargs-parser": "^2.4.1"
+      }
+    },
+    "yargs-parser": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz",
+      "integrity": "sha1-hVaN488VD/SfpRgl8DqMiA3cxcQ=",
+      "requires": {
+        "camelcase": "^3.0.0",
+        "lodash.assign": "^4.0.6"
+      }
+    },
+    "yargs-unparser": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz",
+      "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==",
+      "dev": true,
+      "requires": {
+        "flat": "^4.1.0",
+        "lodash": "^4.17.11",
+        "yargs": "^12.0.5"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "camelcase": {
+          "version": "5.3.1",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+          "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+          "dev": true
+        },
+        "cliui": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
+          "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
+          "dev": true,
+          "requires": {
+            "string-width": "^2.1.1",
+            "strip-ansi": "^4.0.0",
+            "wrap-ansi": "^2.0.0"
+          }
+        },
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "invert-kv": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
+          "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "lcid": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
+          "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
+          "dev": true,
+          "requires": {
+            "invert-kv": "^2.0.0"
+          }
+        },
+        "os-locale": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
+          "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
+          "dev": true,
+          "requires": {
+            "execa": "^1.0.0",
+            "lcid": "^2.0.0",
+            "mem": "^4.0.0"
+          }
+        },
+        "string-width": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+          "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+          "dev": true,
+          "requires": {
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^4.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        },
+        "which-module": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+          "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+          "dev": true
+        },
+        "yargs": {
+          "version": "12.0.5",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
+          "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
+          "dev": true,
+          "requires": {
+            "cliui": "^4.0.0",
+            "decamelize": "^1.2.0",
+            "find-up": "^3.0.0",
+            "get-caller-file": "^1.0.1",
+            "os-locale": "^3.0.0",
+            "require-directory": "^2.1.1",
+            "require-main-filename": "^1.0.1",
+            "set-blocking": "^2.0.0",
+            "string-width": "^2.0.0",
+            "which-module": "^2.0.0",
+            "y18n": "^3.2.1 || ^4.0.0",
+            "yargs-parser": "^11.1.1"
+          }
+        },
+        "yargs-parser": {
+          "version": "11.1.1",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
+          "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
+          "dev": true,
+          "requires": {
+            "camelcase": "^5.0.0",
+            "decamelize": "^1.2.0"
+          }
+        }
+      }
+    },
+    "yauzl": {
+      "version": "2.10.0",
+      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+      "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
+      "requires": {
+        "buffer-crc32": "~0.2.3",
+        "fd-slicer": "~1.1.0"
+      }
+    },
+    "yeast": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+      "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
+    },
+    "yn": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz",
+      "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo="
+    }
+  }
+}
diff --git a/modules/backend/package.json b/modules/backend/package.json
new file mode 100644
index 0000000..a1019e5
--- /dev/null
+++ b/modules/backend/package.json
@@ -0,0 +1,86 @@
+{
+  "name": "ignite-web-console",
+  "version": "2.5.0",
+  "description": "Interactive Web console for configuration, executing SQL queries and monitoring of Apache Ignite Cluster",
+  "private": true,
+  "main": "index.js",
+  "scripts": {
+    "ci-test": "cross-env NODE_ENV=test MOCHA_REPORTER=mocha-teamcity-reporter node ./test/index.js",
+    "test": "cross-env NODE_ENV=test CONFIG_PATH='./test/config/settings.json' node ./test/index.js",
+    "eslint": "eslint --env node --format friendly ./",
+    "start": "node ./index.js",
+    "build": "pkg . --out-path build"
+  },
+  "license": "Apache-2.0",
+  "keywords": [
+    "Apache Ignite Web console"
+  ],
+  "homepage": "https://ignite.apache.org/",
+  "engines": {
+    "npm": ">=5.x.x",
+    "node": ">=8.x.x <10.x.x"
+  },
+  "os": [
+    "darwin",
+    "linux",
+    "win32"
+  ],
+  "bin": "index.js",
+  "pkg": {
+    "assets": [
+      "app/*",
+      "errors/*",
+      "middlewares/*",
+      "migrations/*",
+      "routes/*",
+      "services/*",
+      "templates/*",
+      "node_modules/getos/logic/*",
+      "node_modules/mongodb-download/node_modules/getos/logic/*"
+    ],
+    "scripts": [
+      "app/*.js",
+      "errors/*.js",
+      "middlewares/*.js",
+      "migrations/*.js",
+      "routes/*.js",
+      "services/*.js"
+    ]
+  },
+  "dependencies": {
+    "app-module-path": "2.2.0",
+    "body-parser": "^1.18.3",
+    "connect-mongodb-session": "^2.1.1",
+    "cookie-parser": "^1.4.4",
+    "express": "^4.16.4",
+    "express-mongo-sanitize": "1.3.2",
+    "express-session": "^1.16.1",
+    "fire-up": "1.0.0",
+    "glob": "7.1.3",
+    "jszip": "^3.2.1",
+    "lodash": "4.17.11",
+    "migrate-mongoose-typescript": "^3.3.4",
+    "mongodb-prebuilt": "6.5.0",
+    "mongoose": "^5.5.1",
+    "morgan": "^1.9.1",
+    "nconf": "^0.10.0",
+    "nodemailer": "^6.1.0",
+    "passport": "^0.4.0",
+    "passport-local": "1.0.0",
+    "passport-local-mongoose": "^5.0.1",
+    "passport.socketio": "3.7.0",
+    "pkg": "4.3.8",
+    "socket.io": "2.1.1",
+    "uuid": "^3.3.2"
+  },
+  "devDependencies": {
+    "chai": "4.2.0",
+    "cross-env": "5.2.0",
+    "eslint": "^5.16.0",
+    "eslint-formatter-friendly": "^6.0.0",
+    "mocha": "^6.1.4",
+    "mocha-teamcity-reporter": "^2.5.2",
+    "mongodb-memory-server": "^5.0.4",
+    "supertest": "3.0.0"
+  }
+}
diff --git a/modules/backend/routes/activities.js b/modules/backend/routes/activities.js
new file mode 100644
index 0000000..11fd81b
--- /dev/null
+++ b/modules/backend/routes/activities.js
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const express = require('express');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'routes/activities',
+    inject: ['services/activities'],
+
+    /**
+     * @param {ActivitiesService} activitiesService
+     * @returns {Promise}
+     */
+    factory: (activitiesService) => {
+        return new Promise((factoryResolve) => {
+            const router = new express.Router();
+
+            // Post user activities to page.
+            router.post('/page', (req, res) => {
+                activitiesService.merge(req.user, req.body)
+                    .then(res.api.ok)
+                    .catch(res.api.error);
+            });
+
+            factoryResolve(router);
+        });
+    }
+};
diff --git a/modules/backend/routes/admin.js b/modules/backend/routes/admin.js
new file mode 100644
index 0000000..70f07b0
--- /dev/null
+++ b/modules/backend/routes/admin.js
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const express = require('express');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'routes/admin',
+    inject: ['settings', 'mongo', 'services/spaces', 'services/sessions', 'services/users', 'services/notifications']
+};
+
+/**
+ * @param settings
+ * @param mongo
+ * @param spacesService
+ * @param {SessionsService} sessionsService
+ * @param {UsersService} usersService
+ * @param {NotificationsService} notificationsService
+ * @returns {Promise}
+ */
+module.exports.factory = function(settings, mongo, spacesService, sessionsService, usersService, notificationsService) {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        /**
+         * Get list of user accounts.
+         */
+        router.post('/list', (req, res) => {
+            usersService.list(req.body)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        // Remove user.
+        router.post('/remove', (req, res) => {
+            usersService.remove(req.origin(), req.body.userId)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        // Grant or revoke admin access to user.
+        router.post('/toggle', (req, res) => {
+            const params = req.body;
+
+            mongo.Account.findByIdAndUpdate(params.userId, {admin: params.adminFlag}).exec()
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        // Become user.
+        router.get('/become', (req, res) => {
+            sessionsService.become(req.session, req.query.viewedUserId)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        // Revert to your identity.
+        router.get('/revert/identity', (req, res) => {
+            sessionsService.revert(req.session)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        // Update notifications.
+        router.put('/notifications', (req, res) => {
+            notificationsService.merge(req.user._id, req.body.message, req.body.isShown)
+                .then(res.api.done)
+                .catch(res.api.error);
+        });
+
+        factoryResolve(router);
+    });
+};
+
diff --git a/modules/backend/routes/caches.js b/modules/backend/routes/caches.js
new file mode 100644
index 0000000..25f76a1
--- /dev/null
+++ b/modules/backend/routes/caches.js
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const express = require('express');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'routes/caches',
+    inject: ['mongo', 'services/caches']
+};
+
+module.exports.factory = function(mongo, cachesService) {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        router.get('/:_id', (req, res) => {
+            cachesService.get(req.currentUserId(), req.demo(), req.params._id)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        router.delete('/', (req, res) => {
+            cachesService.remove(req.body.ids)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Save cache.
+         */
+        router.post('/save', (req, res) => {
+            const cache = req.body;
+
+            cachesService.merge(cache)
+                .then((savedCache) => res.api.ok(savedCache._id))
+                .catch(res.api.error);
+        });
+
+        /**
+         * Remove cache by ._id.
+         */
+        router.post('/remove', (req, res) => {
+            const cacheId = req.body._id;
+
+            cachesService.remove(cacheId)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Remove all caches.
+         */
+        router.post('/remove/all', (req, res) => {
+            cachesService.removeAll(req.currentUserId(), req.header('IgniteDemoMode'))
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        factoryResolve(router);
+    });
+};
+
diff --git a/modules/backend/routes/clusters.js b/modules/backend/routes/clusters.js
new file mode 100644
index 0000000..ac7b25e
--- /dev/null
+++ b/modules/backend/routes/clusters.js
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const express = require('express');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'routes/clusters',
+    inject: ['mongo', 'services/clusters', 'services/caches', 'services/domains', 'services/igfss']
+};
+
+module.exports.factory = function(mongo, clustersService, cachesService, domainsService, igfssService) {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        router.get('/:_id/caches', (req, res) => {
+            cachesService.shortList(req.currentUserId(), req.demo(), req.params._id)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        router.get('/:_id/models', (req, res) => {
+            domainsService.shortList(req.currentUserId(), req.demo(), req.params._id)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        router.get('/:_id/igfss', (req, res) => {
+            igfssService.shortList(req.currentUserId(), req.demo(), req.params._id)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        router.get('/:_id', (req, res) => {
+            clustersService.get(req.currentUserId(), req.demo(), req.params._id)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        router.get('/', (req, res) => {
+            clustersService.shortList(req.currentUserId(), req.demo())
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        router.put('/basic', (req, res) => {
+            clustersService.upsertBasic(req.currentUserId(), req.demo(), req.body)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        router.put('/', (req, res) => {
+            clustersService.upsert(req.currentUserId(), req.demo(), req.body)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Save cluster.
+         */
+        router.post('/save', (req, res) => {
+            const cluster = req.body;
+
+            clustersService.merge(cluster)
+                .then((savedCluster) => res.api.ok(savedCluster._id))
+                .catch(res.api.error);
+        });
+
+        /**
+         * Remove cluster by ._id.
+         */
+        router.post('/remove', (req, res) => {
+            const clusterId = req.body._id;
+
+            clustersService.remove(clusterId)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Remove all clusters.
+         */
+        router.post('/remove/all', (req, res) => {
+            clustersService.removeAll(req.currentUserId(), req.header('IgniteDemoMode'))
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        factoryResolve(router);
+    });
+};
diff --git a/modules/backend/routes/configuration.js b/modules/backend/routes/configuration.js
new file mode 100644
index 0000000..9ec002b
--- /dev/null
+++ b/modules/backend/routes/configuration.js
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const express = require('express');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'routes/configurations',
+    inject: ['mongo', 'services/configurations']
+};
+
+module.exports.factory = function(mongo, configurationsService) {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        /**
+         * Get all user configuration in current space.
+         */
+        router.get('/list', (req, res) => {
+            configurationsService.list(req.currentUserId(), req.demo())
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Get user configuration in current space.
+         */
+        router.get('/:_id', (req, res) => {
+            configurationsService.get(req.currentUserId(), req.demo(), req.params._id)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        factoryResolve(router);
+    });
+};
diff --git a/modules/backend/routes/demo.js b/modules/backend/routes/demo.js
new file mode 100644
index 0000000..b081d0c
--- /dev/null
+++ b/modules/backend/routes/demo.js
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const express = require('express');
+const _ = require('lodash');
+
+// Fire me up!
+
+const clusters = require('./demo/clusters.json');
+const caches = require('./demo/caches.json');
+const domains = require('./demo/domains.json');
+const igfss = require('./demo/igfss.json');
+
+module.exports = {
+    implements: 'routes/demo',
+    inject: ['errors', 'settings', 'mongo', 'services/spaces']
+};
+
+/**
+ *
+ * @param _
+ * @param express
+ * @param errors
+ * @param settings
+ * @param mongo
+ * @param spacesService
+ * @return {Promise}
+ */
+module.exports.factory = (errors, settings, mongo, spacesService) => {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        /**
+         * Reset demo configuration.
+         */
+        router.post('/reset', (req, res) => {
+            spacesService.spaces(req.user._id, true)
+                .then((spaces) => {
+                    const spaceIds = _.map(spaces, '_id');
+
+                    return spacesService.cleanUp(spaceIds)
+                        .then(() => mongo.Space.remove({_id: {$in: _.tail(spaceIds)}}).exec())
+                        .then(() => _.head(spaces));
+                })
+                .catch((err) => {
+                    if (err instanceof errors.MissingResourceException)
+                        return spacesService.createDemoSpace(req.user._id);
+
+                    throw err;
+                })
+                .then((space) => {
+                    return Promise.all(_.map(clusters, (cluster) => {
+                        const clusterDoc = new mongo.Cluster(cluster);
+
+                        clusterDoc.space = space._id;
+
+                        return clusterDoc.save();
+                    }));
+                })
+                .then((clusterDocs) => {
+                    return _.map(clusterDocs, (cluster) => {
+                        const addCacheToCluster = (cacheDoc) => cluster.caches.push(cacheDoc._id);
+                        const addIgfsToCluster = (igfsDoc) => cluster.igfss.push(igfsDoc._id);
+
+                        if (cluster.name.endsWith('-caches')) {
+                            const cachePromises = _.map(caches, (cacheData) => {
+                                const cache = new mongo.Cache(cacheData);
+
+                                cache.space = cluster.space;
+                                cache.clusters.push(cluster._id);
+
+                                return cache.save()
+                                    .then((cacheDoc) => {
+                                        const domainData = _.find(domains, (item) =>
+                                            item.databaseTable === cacheDoc.name.slice(0, -5).toUpperCase());
+
+                                        if (domainData) {
+                                            const domain = new mongo.DomainModel(domainData);
+
+                                            domain.space = cacheDoc.space;
+                                            domain.caches.push(cacheDoc._id);
+                                            domain.clusters.push(cluster._id);
+
+                                            return domain.save()
+                                                .then((domainDoc) => {
+                                                    cacheDoc.domains.push(domainDoc._id);
+                                                    cluster.models.push(domainDoc._id);
+
+                                                    return cacheDoc.save();
+                                                });
+                                        }
+
+                                        return cacheDoc;
+                                    });
+                            });
+
+                            return Promise.all(cachePromises)
+                                .then((cacheDocs) => {
+                                    _.forEach(cacheDocs, addCacheToCluster);
+
+                                    return cluster.save();
+                                });
+                        }
+
+                        if (cluster.name.endsWith('-igfs')) {
+                            return Promise.all(_.map(igfss, (igfs) => {
+                                const igfsDoc = new mongo.Igfs(igfs);
+
+                                igfsDoc.space = cluster.space;
+                                igfsDoc.clusters.push(cluster._id);
+
+                                return igfsDoc.save();
+                            }))
+                            .then((igfsDocs) => {
+                                _.forEach(igfsDocs, addIgfsToCluster);
+
+                                return cluster.save();
+                            });
+                        }
+                    });
+                })
+                .then(() => res.sendStatus(200))
+                .catch((err) => res.status(500).send(err.message));
+        });
+
+        factoryResolve(router);
+    });
+};
+
diff --git a/modules/backend/routes/demo/caches.json b/modules/backend/routes/demo/caches.json
new file mode 100644
index 0000000..f7a8690
--- /dev/null
+++ b/modules/backend/routes/demo/caches.json
@@ -0,0 +1,87 @@
+[
+  {
+    "name": "CarCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": [],
+    "clusters": []
+  },
+  {
+    "name": "ParkingCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": [],
+    "clusters": []
+  },
+  {
+    "name": "CountryCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": [],
+    "clusters": []
+  },
+  {
+    "name": "DepartmentCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": [],
+    "clusters": []
+  },
+  {
+    "name": "EmployeeCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": [],
+    "clusters": []
+  }
+]
diff --git a/modules/backend/routes/demo/clusters.json b/modules/backend/routes/demo/clusters.json
new file mode 100644
index 0000000..014b519
--- /dev/null
+++ b/modules/backend/routes/demo/clusters.json
@@ -0,0 +1,50 @@
+[
+  {
+    "name": "cluster-igfs",
+    "connector": {
+      "noDelay": true
+    },
+    "communication": {
+      "tcpNoDelay": true
+    },
+    "igfss": [],
+    "caches": [],
+    "binaryConfiguration": {
+      "compactFooter": true,
+      "typeConfigurations": []
+    },
+    "discovery": {
+      "kind": "Multicast",
+      "Multicast": {
+        "addresses": ["127.0.0.1:47500..47510"]
+      },
+      "Vm": {
+        "addresses": ["127.0.0.1:47500..47510"]
+      }
+    }
+  },
+  {
+    "name": "cluster-caches",
+    "connector": {
+      "noDelay": true
+    },
+    "communication": {
+      "tcpNoDelay": true
+    },
+    "igfss": [],
+    "caches": [],
+    "binaryConfiguration": {
+      "compactFooter": true,
+      "typeConfigurations": []
+    },
+    "discovery": {
+      "kind": "Multicast",
+      "Multicast": {
+        "addresses": ["127.0.0.1:47500..47510"]
+      },
+      "Vm": {
+        "addresses": ["127.0.0.1:47500..47510"]
+      }
+    }
+  }
+]
diff --git a/modules/backend/routes/demo/domains.json b/modules/backend/routes/demo/domains.json
new file mode 100644
index 0000000..25f5019
--- /dev/null
+++ b/modules/backend/routes/demo/domains.json
@@ -0,0 +1,317 @@
+[
+  {
+    "keyType": "Integer",
+    "valueType": "model.Parking",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "CARS",
+    "databaseTable": "PARKING",
+    "indexes": [],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "name",
+        "className": "String"
+      },
+      {
+        "name": "capacity",
+        "className": "Integer"
+      }
+    ],
+    "keyFieldName": "id",
+    "valueFields": [
+      {
+        "databaseFieldName": "NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "name",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "CAPACITY",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "capacity",
+        "javaFieldType": "int"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": [],
+    "generatePojo": true
+  },
+  {
+    "keyType": "Integer",
+    "valueType": "model.Department",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "PUBLIC",
+    "databaseTable": "DEPARTMENT",
+    "indexes": [],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "countryId",
+        "className": "Integer"
+      },
+      {
+        "name": "name",
+        "className": "String"
+      }
+    ],
+    "keyFieldName": "id",
+    "valueFields": [
+      {
+        "databaseFieldName": "COUNTRY_ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "countryId",
+        "javaFieldType": "int"
+      },
+      {
+        "databaseFieldName": "NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "name",
+        "javaFieldType": "String"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": [],
+    "generatePojo": true
+  },
+  {
+    "keyType": "Integer",
+    "valueType": "model.Employee",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "PUBLIC",
+    "databaseTable": "EMPLOYEE",
+    "indexes": [
+      {
+        "name": "EMP_NAMES",
+        "indexType": "SORTED",
+        "fields": [
+          {
+            "name": "firstName",
+            "direction": true
+          },
+          {
+            "name": "lastName",
+            "direction": true
+          }
+        ]
+      },
+      {
+        "name": "EMP_SALARY",
+        "indexType": "SORTED",
+        "fields": [
+          {
+            "name": "salary",
+            "direction": true
+          }
+        ]
+      }
+    ],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "departmentId",
+        "className": "Integer"
+      },
+      {
+        "name": "managerId",
+        "className": "Integer"
+      },
+      {
+        "name": "firstName",
+        "className": "String"
+      },
+      {
+        "name": "lastName",
+        "className": "String"
+      },
+      {
+        "name": "email",
+        "className": "String"
+      },
+      {
+        "name": "phoneNumber",
+        "className": "String"
+      },
+      {
+        "name": "hireDate",
+        "className": "Date"
+      },
+      {
+        "name": "job",
+        "className": "String"
+      },
+      {
+        "name": "salary",
+        "className": "Double"
+      }
+    ],
+    "keyFieldName": "id",
+    "valueFields": [
+      {
+        "databaseFieldName": "DEPARTMENT_ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "departmentId",
+        "javaFieldType": "int"
+      },
+      {
+        "databaseFieldName": "MANAGER_ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "managerId",
+        "javaFieldType": "Integer"
+      },
+      {
+        "databaseFieldName": "FIRST_NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "firstName",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "LAST_NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "lastName",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "EMAIL",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "email",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "PHONE_NUMBER",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "phoneNumber",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "HIRE_DATE",
+        "databaseFieldType": "DATE",
+        "javaFieldName": "hireDate",
+        "javaFieldType": "Date"
+      },
+      {
+        "databaseFieldName": "JOB",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "job",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "SALARY",
+        "databaseFieldType": "DOUBLE",
+        "javaFieldName": "salary",
+        "javaFieldType": "Double"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": [],
+    "generatePojo": true
+  },
+  {
+    "keyType": "Integer",
+    "valueType": "model.Country",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "PUBLIC",
+    "databaseTable": "COUNTRY",
+    "indexes": [],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "name",
+        "className": "String"
+      },
+      {
+        "name": "population",
+        "className": "Integer"
+      }
+    ],
+    "keyFieldName": "id",
+    "valueFields": [
+      {
+        "databaseFieldName": "NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "name",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "POPULATION",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "population",
+        "javaFieldType": "int"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": [],
+    "generatePojo": true
+  },
+  {
+    "keyType": "Integer",
+    "valueType": "model.Car",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "CARS",
+    "databaseTable": "CAR",
+    "indexes": [],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "parkingId",
+        "className": "Integer"
+      },
+      {
+        "name": "name",
+        "className": "String"
+      }
+    ],
+    "keyFieldName": "id",
+    "valueFields": [
+      {
+        "databaseFieldName": "PARKING_ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "parkingId",
+        "javaFieldType": "int"
+      },
+      {
+        "databaseFieldName": "NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "name",
+        "javaFieldType": "String"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": [],
+    "generatePojo": true
+  }
+]
diff --git a/modules/backend/routes/demo/igfss.json b/modules/backend/routes/demo/igfss.json
new file mode 100644
index 0000000..cd128a6
--- /dev/null
+++ b/modules/backend/routes/demo/igfss.json
@@ -0,0 +1,10 @@
+[
+  {
+    "ipcEndpointEnabled": true,
+    "fragmentizerEnabled": true,
+    "name": "igfs",
+    "dataCacheName": "igfs-data",
+    "metaCacheName": "igfs-meta",
+    "clusters": []
+  }
+]
diff --git a/modules/backend/routes/domains.js b/modules/backend/routes/domains.js
new file mode 100644
index 0000000..9360421
--- /dev/null
+++ b/modules/backend/routes/domains.js
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const express = require('express');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'routes/domains',
+    inject: ['mongo', 'services/domains']
+};
+
+module.exports.factory = (mongo, domainsService) => {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        router.get('/:_id', (req, res) => {
+            domainsService.get(req.currentUserId(), req.demo(), req.params._id)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Save domain model.
+         */
+        router.post('/save', (req, res) => {
+            const domain = req.body;
+
+            domainsService.batchMerge([domain])
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Batch save domain models.
+         */
+        router.post('/save/batch', (req, res) => {
+            const domains = req.body;
+
+            domainsService.batchMerge(domains)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Remove domain model by ._id.
+         */
+        router.post('/remove', (req, res) => {
+            const domainId = req.body._id;
+
+            domainsService.remove(domainId)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Remove all domain models.
+         */
+        router.post('/remove/all', (req, res) => {
+            domainsService.removeAll(req.currentUserId(), req.header('IgniteDemoMode'))
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        factoryResolve(router);
+    });
+};
+
diff --git a/modules/backend/routes/downloads.js b/modules/backend/routes/downloads.js
new file mode 100644
index 0000000..a06bb27
--- /dev/null
+++ b/modules/backend/routes/downloads.js
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const express = require('express');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'routes/downloads',
+    inject: ['services/agents', 'services/activities']
+};
+
+/**
+ * @param _
+ * @param express
+ * @param {DownloadsService} downloadsService
+ * @param {ActivitiesService} activitiesService
+ * @returns {Promise}
+ */
+module.exports.factory = function(downloadsService, activitiesService) {
+    return new Promise((resolveFactory) => {
+        const router = new express.Router();
+
+        /* Get grid topology. */
+        router.get('/agent', (req, res) => {
+            activitiesService.merge(req.user._id, {
+                group: 'agent',
+                action: '/agent/download'
+            });
+
+            downloadsService.prepareArchive(req.origin(), req.user.token)
+                .then(({fileName, buffer}) => {
+                    // Set the archive name.
+                    res.attachment(fileName);
+
+                    res.send(buffer);
+                })
+                .catch(res.api.error);
+        });
+
+        resolveFactory(router);
+    });
+};
diff --git a/modules/backend/routes/igfss.js b/modules/backend/routes/igfss.js
new file mode 100644
index 0000000..c975249
--- /dev/null
+++ b/modules/backend/routes/igfss.js
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const express = require('express');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'routes/igfss',
+    inject: ['mongo', 'services/igfss']
+};
+
+module.exports.factory = function(mongo, igfssService) {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        router.get('/:_id', (req, res) => {
+            igfssService.get(req.currentUserId(), req.demo(), req.params._id)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        router.delete('/', (req, res) => {
+            igfssService.remove(req.body.ids)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Save IGFS.
+         */
+        router.post('/save', (req, res) => {
+            const igfs = req.body;
+
+            igfssService.merge(igfs)
+                .then((savedIgfs) => res.api.ok(savedIgfs._id))
+                .catch(res.api.error);
+        });
+
+        /**
+         * Remove IGFS by ._id.
+         */
+        router.post('/remove', (req, res) => {
+            const igfsId = req.body._id;
+
+            igfssService.remove(igfsId)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Remove all IGFSs.
+         */
+        router.post('/remove/all', (req, res) => {
+            igfssService.removeAll(req.currentUserId(), req.header('IgniteDemoMode'))
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        factoryResolve(router);
+    });
+};
+
diff --git a/modules/backend/routes/notebooks.js b/modules/backend/routes/notebooks.js
new file mode 100644
index 0000000..0807db8
--- /dev/null
+++ b/modules/backend/routes/notebooks.js
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const express = require('express');
+const _ = require('lodash');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'routes/notebooks',
+    inject: ['mongo', 'services/spaces', 'services/notebooks']
+};
+
+module.exports.factory = (mongo, spacesService, notebooksService) => {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        /**
+         * Get notebooks names accessed for user account.
+         *
+         * @param req Request.
+         * @param res Response.
+         */
+        router.get('/', (req, res) => {
+            return spacesService.spaces(req.currentUserId())
+                .then((spaces) => _.map(spaces, (space) => space._id))
+                .then((spaceIds) => notebooksService.listBySpaces(spaceIds))
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Save notebook accessed for user account.
+         *
+         * @param req Request.
+         * @param res Response.
+         */
+        router.post('/save', (req, res) => {
+            const notebook = req.body;
+
+            spacesService.spaceIds(req.currentUserId())
+                .then((spaceIds) => {
+                    notebook.space = notebook.space || spaceIds[0];
+
+                    return notebooksService.merge(notebook);
+                })
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Remove notebook by ._id.
+         *
+         * @param req Request.
+         * @param res Response.
+         */
+        router.post('/remove', (req, res) => {
+            const notebookId = req.body._id;
+
+            notebooksService.remove(notebookId)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        factoryResolve(router);
+    });
+};
diff --git a/modules/backend/routes/profile.js b/modules/backend/routes/profile.js
new file mode 100644
index 0000000..79fb3de
--- /dev/null
+++ b/modules/backend/routes/profile.js
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const express = require('express');
+const _ = require('lodash');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'routes/profiles',
+    inject: ['mongo', 'services/users']
+};
+
+/**
+ * @param mongo
+ * @param {UsersService} usersService
+ * @returns {Promise}
+ */
+module.exports.factory = function(mongo, usersService) {
+    return new Promise((resolveFactory) => {
+        const router = new express.Router();
+
+        /**
+         * Save user profile.
+         */
+        router.post('/save', (req, res) => {
+            if (req.body.password && _.isEmpty(req.body.password))
+                return res.status(500).send('Wrong value for new password!');
+
+            usersService.save(req.user._id, req.body)
+                .then((user) => {
+                    const becomeUsed = req.session.viewedUser && req.user.admin;
+
+                    if (becomeUsed) {
+                        req.session.viewedUser = user;
+
+                        return req.user;
+                    }
+
+                    return new Promise((resolve, reject) => {
+                        req.logout();
+
+                        req.logIn(user, {}, (errLogIn) => {
+                            if (errLogIn)
+                                return reject(errLogIn);
+
+                            return resolve(user);
+                        });
+                    });
+                })
+                .then((user) => usersService.get(user, req.session.viewedUser))
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        resolveFactory(router);
+    });
+};
diff --git a/modules/backend/routes/public.js b/modules/backend/routes/public.js
new file mode 100644
index 0000000..d290b53
--- /dev/null
+++ b/modules/backend/routes/public.js
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const _ = require('lodash');
+const express = require('express');
+const passport = require('passport');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'routes/public',
+    inject: ['mongo', 'settings', 'services/users', 'services/auth', 'errors']
+};
+
+/**                             
+ * @param mongo
+ * @param settings
+ * @param {UsersService} usersService
+ * @param {AuthService} authService
+ * @param errors
+ * @returns {Promise}
+ */
+module.exports.factory = function(mongo, settings, usersService, authService, errors) {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        // GET user.
+        router.post('/user', (req, res) => {
+            usersService.get(req.user, req.session.viewedUser)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Register new account.
+         */
+        router.post('/signup', (req, res) => {
+            const createdByAdmin = _.get(req, 'user.admin', false);
+
+            usersService.create(req.origin(), req.body, createdByAdmin)
+                .then((user) => {
+                    if (createdByAdmin)
+                        return user;
+
+                    return new Promise((resolve, reject) => {
+                        req.logIn(user, {}, (err) => {
+                            if (err)
+                                reject(err);
+
+                            resolve(user);
+                        });
+                    });
+                })
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /**
+         * Sign in into exist account.
+         */
+        router.post('/signin', (req, res, next) => {
+            passport.authenticate('local', (errAuth, user) => {
+                if (errAuth)
+                    return res.api.error(new errors.AuthFailedException(errAuth.message));
+
+                if (!user)
+                    return res.api.error(new errors.AuthFailedException('Invalid email or password'));
+
+                if (settings.activation.enabled) {
+                    const activationToken = req.body.activationToken;
+
+                    const errToken = authService.validateActivationToken(user, activationToken);
+
+                    if (errToken)
+                        return res.api.error(errToken);
+
+                    if (authService.isActivationTokenExpired(user, activationToken)) {
+                        authService.resetActivationToken(req.origin(), user.email)
+                            .catch((ignored) => {
+                                // No-op.
+                            });
+
+                        return res.api.error(new errors.AuthFailedException('This activation link was expired. We resend a new one. Please open the most recent email and click on the activation link.'));
+                    }
+
+                    user.activated = true;
+                }
+
+                return req.logIn(user, {}, (errLogIn) => {
+                    if (errLogIn)
+                        return res.api.error(new errors.AuthFailedException(errLogIn.message));
+
+                    return res.sendStatus(200);
+                });
+            })(req, res, next);
+        });
+
+        /**
+         * Logout.
+         */
+        router.post('/logout', (req, res) => {
+            req.logout();
+
+            res.sendStatus(200);
+        });
+
+        /**
+         * Send e-mail to user with reset token.
+         */
+        router.post('/password/forgot', (req, res) => {
+            authService.resetPasswordToken(req.origin(), req.body.email)
+                .then(() => res.api.ok('An email has been sent with further instructions.'))
+                .catch(res.api.error);
+        });
+
+        /**
+         * Change password with given token.
+         */
+        router.post('/password/reset', (req, res) => {
+            const {token, password} = req.body;
+
+            authService.resetPasswordByToken(req.origin(), token, password)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /* GET reset password page. */
+        router.post('/password/validate/token', (req, res) => {
+            const token = req.body.token;
+
+            authService.validateResetToken(token)
+                .then(res.api.ok)
+                .catch(res.api.error);
+        });
+
+        /* Send e-mail to user with account confirmation token. */
+        router.post('/activation/resend', (req, res) => {
+            authService.resetActivationToken(req.origin(), req.body.email)
+                .then(() => res.api.ok('An email has been sent with further instructions.'))
+                .catch(res.api.error);
+        });
+
+        factoryResolve(router);
+    });
+};
diff --git a/modules/backend/services/Utils.js b/modules/backend/services/Utils.js
new file mode 100644
index 0000000..ec97e4e
--- /dev/null
+++ b/modules/backend/services/Utils.js
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/utils'
+};
+
+/**
+ * @returns {UtilsService}
+ */
+module.exports.factory = () => {
+    class UtilsService {
+        /**
+         * Generate token string.
+         *
+         * @param len length of string
+         * @returns {String} Random string.
+         */
+        static randomString(len) {
+            const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+            const possibleLen = possible.length;
+
+            let res = '';
+
+            for (let i = 0; i < len; i++)
+                res += possible.charAt(Math.floor(Math.random() * possibleLen));
+
+            return res;
+        }
+    }
+
+    return UtilsService;
+};
diff --git a/modules/backend/services/activities.js b/modules/backend/services/activities.js
new file mode 100644
index 0000000..bc0245d
--- /dev/null
+++ b/modules/backend/services/activities.js
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const _ = require('lodash');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/activities',
+    inject: ['mongo']
+};
+
+/**
+ * @param mongo
+ * @returns {ActivitiesService}
+ */
+module.exports.factory = (mongo) => {
+    class ActivitiesService {
+        /**
+         * Update page activities.
+         *
+         * @param {Object} user - User.
+         * @param {String} action - Action string presentation.
+         * @param {String} group - Action group string presentation.
+         * @param {Date} [now] - Optional date to save in activity.
+         * @returns {Promise.<mongo.ObjectId>} that resolve activity.
+         */
+        static merge(user, {action, group}, now = new Date()) {
+            const owner = user._id;
+
+            mongo.Account.findById(owner)
+                .then((user) => {
+                    user.lastActivity = new Date();
+
+                    return user.save();
+                });
+
+            const date = Date.UTC(now.getFullYear(), now.getMonth(), 1);
+
+            return mongo.Activities.findOneAndUpdate({owner, action, date},
+                {$set: {owner, group, action, date}, $inc: {amount: 1}}, {new: true, upsert: true}).exec();
+        }
+
+        static total({startDate, endDate}) {
+            const $match = {};
+
+            if (startDate)
+                $match.date = {$gte: new Date(startDate)};
+
+            if (endDate) {
+                $match.date = $match.date || {};
+                $match.date.$lt = new Date(endDate);
+            }
+
+            return mongo.Activities.aggregate([
+                {$match},
+                {$group: {
+                    _id: {owner: '$owner', group: '$group'},
+                    amount: {$sum: '$amount'}
+                }}
+            ]).exec().then((data) => {
+                return _.reduce(data, (acc, { _id, amount }) => {
+                    const {owner, group} = _id;
+                    acc[owner] = _.merge(acc[owner] || {}, { [group]: amount });
+                    return acc;
+                }, {});
+            });
+        }
+
+        static detail({startDate, endDate}) {
+            const $match = { };
+
+            if (startDate)
+                $match.date = {$gte: new Date(startDate)};
+
+            if (endDate) {
+                $match.date = $match.date || {};
+                $match.date.$lt = new Date(endDate);
+            }
+
+            return mongo.Activities.aggregate([
+                {$match},
+                {$group: {_id: {owner: '$owner', action: '$action'}, total: {$sum: '$amount'}}}
+            ]).exec().then((data) => {
+                return _.reduce(data, (acc, { _id, total }) => {
+                    const {owner, action} = _id;
+                    acc[owner] = _.merge(acc[owner] || {}, { [action]: total });
+                    return acc;
+                }, {});
+            });
+        }
+    }
+
+    return ActivitiesService;
+};
diff --git a/modules/backend/services/auth.js b/modules/backend/services/auth.js
new file mode 100644
index 0000000..e2a9def
--- /dev/null
+++ b/modules/backend/services/auth.js
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+const _ = require('lodash');
+
+module.exports = {
+    implements: 'services/auth',
+    inject: ['mongo', 'settings', 'errors', 'services/utils', 'services/mails']
+};
+
+/**
+ * @param mongo
+ * @param settings
+ * @param errors
+ * @param {UtilsService} utilsService
+ * @param {MailsService} mailsService
+ * @returns {AuthService}
+ */
+
+module.exports.factory = (mongo, settings, errors, utilsService, mailsService) => {
+    class AuthService {
+        /**
+         * Reset password reset token for user.
+         *
+         * @param host Web Console host.
+         * @param email - user email
+         * @returns {Promise.<mongo.Account>} - that resolves account found by email with new reset password token.
+         */
+        static resetPasswordToken(host, email) {
+            return mongo.Account.findOne({email}).exec()
+                .then((user) => {
+                    if (!user)
+                        throw new errors.MissingResourceException('Account with that email address does not exists!');
+
+                    if (settings.activation.enabled && !user.activated)
+                        throw new errors.MissingConfirmRegistrationException(user.email);
+
+                    user.resetPasswordToken = utilsService.randomString(settings.tokenLength);
+
+                    return user.save();
+                })
+                .then((user) => mailsService.sendResetLink(host, user)
+                    .then(() => user));
+        }
+
+        /**
+         * Reset password by reset token.
+         *
+         * @param host Web Console host.
+         * @param {string} token - reset token
+         * @param {string} newPassword - new password
+         * @returns {Promise.<mongo.Account>} - that resolves account with new password
+         */
+        static resetPasswordByToken(host, token, newPassword) {
+            return mongo.Account.findOne({resetPasswordToken: token}).exec()
+                .then((user) => {
+                    if (!user)
+                        throw new errors.MissingResourceException('Failed to find account with this token! Please check link from email.');
+
+                    if (settings.activation.enabled && !user.activated)
+                        throw new errors.MissingConfirmRegistrationException(user.email);
+
+                    return new Promise((resolve, reject) => {
+                        user.setPassword(newPassword, (err, _user) => {
+                            if (err)
+                                return reject(new errors.AppErrorException('Failed to reset password: ' + err.message));
+
+                            _user.resetPasswordToken = undefined; // eslint-disable-line no-undefined
+
+                            resolve(_user.save());
+                        });
+                    });
+                })
+                .then((user) => mailsService.sendPasswordChanged(host, user)
+                    .then(() => user));
+        }
+
+        /**
+         * Find account by token.
+         *
+         * @param {string} token - reset token
+         * @returns {Promise.<{token, email}>} - that resolves token and user email
+         */
+        static validateResetToken(token) {
+            return mongo.Account.findOne({resetPasswordToken: token}).exec()
+                .then((user) => {
+                    if (!user)
+                        throw new errors.IllegalAccessError('Invalid token for password reset!');
+
+                    if (settings.activation.enabled && !user.activated)
+                        throw new errors.MissingConfirmRegistrationException(user.email);
+
+                    return {token, email: user.email};
+                });
+        }
+
+        /**
+         * Validate activationToken token.
+         *
+         * @param {mongo.Account} user - User object.
+         * @param {string} activationToken - activate account token
+         * @return {Error} If token is invalid.
+         */
+        static validateActivationToken(user, activationToken) {
+            if (user.activated) {
+                if (!_.isEmpty(activationToken) && user.activationToken !== activationToken)
+                    return new errors.AuthFailedException('Invalid email or password!');
+            }
+            else {
+                if (_.isEmpty(activationToken))
+                    return new errors.MissingConfirmRegistrationException(user.email);
+
+                if (user.activationToken !== activationToken)
+                    return new errors.AuthFailedException('This activation token isn\'t valid.');
+            }
+        }
+
+        /**
+         * Check if activation token expired.
+         *
+         * @param {mongo.Account} user - User object.
+         * @param {string} activationToken - activate account token
+         * @return {boolean} If token was already expired.
+         */
+        static isActivationTokenExpired(user, activationToken) {
+            return !user.activated &&
+                new Date().getTime() - user.activationSentAt.getTime() >= settings.activation.timeout;
+        }
+
+        /**
+         * Reset password reset token for user.
+         *
+         * @param host Web Console host.
+         * @param email - user email.
+         * @returns {Promise}.
+         */
+        static resetActivationToken(host, email) {
+            return mongo.Account.findOne({email}).exec()
+                .then((user) => {
+                    if (!user)
+                        throw new errors.MissingResourceException('Account with that email address does not exists!');
+
+                    if (!settings.activation.enabled)
+                        throw new errors.IllegalAccessError('Activation was not enabled!');
+
+                    if (user.activationSentAt &&
+                        new Date().getTime() - user.activationSentAt.getTime() < settings.activation.sendTimeout)
+                        throw new errors.IllegalAccessError('Too Many Activation Attempts!');
+
+                    user.activationToken = utilsService.randomString(settings.tokenLength);
+                    user.activationSentAt = new Date();
+
+                    return user.save();
+                })
+                .then((user) => mailsService.sendActivationLink(host, user));
+        }
+    }
+
+    return AuthService;
+};
diff --git a/modules/backend/services/caches.js b/modules/backend/services/caches.js
new file mode 100644
index 0000000..6909757
--- /dev/null
+++ b/modules/backend/services/caches.js
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const _ = require('lodash');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/caches',
+    inject: ['mongo', 'services/spaces', 'errors']
+};
+
+/**
+ * @param mongo
+ * @param {SpacesService} spacesService
+ * @param errors
+ * @returns {CachesService}
+ */
+module.exports.factory = (mongo, spacesService, errors) => {
+    /**
+     * Convert remove status operation to own presentation.
+     *
+     * @param {RemoveResult} result - The results of remove operation.
+     */
+    const convertRemoveStatus = (result) => ({rowsAffected: result.n});
+
+    /**
+     * Update existing cache.
+     *
+     * @param {Object} cache - The cache for updating.
+     * @returns {Promise.<mongo.ObjectId>} that resolves cache id.
+     */
+    const update = (cache) => {
+        const cacheId = cache._id;
+
+        return mongo.Cache.updateOne({_id: cacheId}, cache, {upsert: true}).exec()
+            .then(() => mongo.Cluster.updateMany({_id: {$in: cache.clusters}}, {$addToSet: {caches: cacheId}}).exec())
+            .then(() => mongo.Cluster.updateMany({_id: {$nin: cache.clusters}}, {$pull: {caches: cacheId}}).exec())
+            .then(() => mongo.DomainModel.updateMany({_id: {$in: cache.domains}}, {$addToSet: {caches: cacheId}}).exec())
+            .then(() => mongo.DomainModel.updateMany({_id: {$nin: cache.domains}}, {$pull: {caches: cacheId}}).exec())
+            .then(() => cache)
+            .catch((err) => {
+                if (err.code === mongo.errCodes.DUPLICATE_KEY_UPDATE_ERROR || err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                    throw new errors.DuplicateKeyException('Cache with name: "' + cache.name + '" already exist.');
+                else
+                    throw err;
+            });
+    };
+
+    /**
+     * Create new cache.
+     *
+     * @param {Object} cache - The cache for creation.
+     * @returns {Promise.<mongo.ObjectId>} that resolves cache id.
+     */
+    const create = (cache) => {
+        return mongo.Cache.create(cache)
+            .then((savedCache) =>
+                mongo.Cluster.updateMany({_id: {$in: savedCache.clusters}}, {$addToSet: {caches: savedCache._id}}).exec()
+                    .then(() => mongo.DomainModel.updateMany({_id: {$in: savedCache.domains}}, {$addToSet: {caches: savedCache._id}}).exec())
+                    .then(() => savedCache)
+            )
+            .catch((err) => {
+                if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                    throw new errors.DuplicateKeyException('Cache with name: "' + cache.name + '" already exist.');
+                else
+                    throw err;
+            });
+    };
+
+    /**
+     * Remove all caches by space ids.
+     *
+     * @param {Number[]} spaceIds - The space ids for cache deletion.
+     * @returns {Promise.<RemoveResult>} - that resolves results of remove operation.
+     */
+    const removeAllBySpaces = (spaceIds) => {
+        return mongo.Cluster.updateMany({space: {$in: spaceIds}}, {caches: []}).exec()
+            .then(() => mongo.Cluster.updateMany({space: {$in: spaceIds}}, {$pull: {checkpointSpi: {kind: 'Cache'}}}).exec())
+            .then(() => mongo.DomainModel.updateMany({space: {$in: spaceIds}}, {caches: []}).exec())
+            .then(() => mongo.Cache.deleteMany({space: {$in: spaceIds}}).exec());
+    };
+
+    /**
+     * Service for manipulate Cache entities.
+     */
+    class CachesService {
+        static shortList(userId, demo, clusterId) {
+            return spacesService.spaceIds(userId, demo)
+                .then((spaceIds) => mongo.Cache.find({space: {$in: spaceIds}, clusters: clusterId }).select('name cacheMode atomicityMode backups').lean().exec());
+        }
+
+        static get(userId, demo, _id) {
+            return spacesService.spaceIds(userId, demo)
+                .then((spaceIds) => mongo.Cache.findOne({space: {$in: spaceIds}, _id}).lean().exec());
+        }
+
+        static upsertBasic(cache) {
+            if (_.isNil(cache._id))
+                return Promise.reject(new errors.IllegalArgumentException('Cache id can not be undefined or null'));
+
+            const query = _.pick(cache, ['space', '_id']);
+            const newDoc = _.pick(cache, ['space', '_id', 'name', 'cacheMode', 'atomicityMode', 'backups', 'clusters']);
+
+            return mongo.Cache.updateOne(query, {$set: newDoc}, {upsert: true}).exec()
+                .catch((err) => {
+                    if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                        throw new errors.DuplicateKeyException(`Cache with name: "${cache.name}" already exist.`);
+
+                    throw err;
+                })
+                .then((updated) => {
+                    if (updated.nModified === 0)
+                        return mongo.Cache.updateOne(query, {$set: cache}, {upsert: true}).exec();
+
+                    return updated;
+                });
+        }
+
+        static upsert(cache) {
+            if (_.isNil(cache._id))
+                return Promise.reject(new errors.IllegalArgumentException('Cache id can not be undefined or null'));
+
+            const query = _.pick(cache, ['space', '_id']);
+
+            return mongo.Cache.updateOne(query, {$set: cache}, {upsert: true}).exec()
+                .then(() => mongo.DomainModel.updateMany({_id: {$in: cache.domains}}, {$addToSet: {caches: cache._id}}).exec())
+                .then(() => mongo.DomainModel.updateMany({_id: {$nin: cache.domains}}, {$pull: {caches: cache._id}}).exec())
+                .catch((err) => {
+                    if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                        throw new errors.DuplicateKeyException(`Cache with name: "${cache.name}" already exist.`);
+
+                    throw err;
+                });
+        }
+
+        /**
+         * Create or update cache.
+         *
+         * @param {Object} cache - The cache.
+         * @returns {Promise.<mongo.ObjectId>} that resolves cache id of merge operation.
+         */
+        static merge(cache) {
+            if (cache._id)
+                return update(cache);
+
+            return create(cache);
+        }
+
+        /**
+         * Get caches by spaces.
+         *
+         * @param {mongo.ObjectId|String} spaceIds - The spaces ids that own caches.
+         * @returns {Promise.<mongo.Cache[]>} - contains requested caches.
+         */
+        static listBySpaces(spaceIds) {
+            return mongo.Cache.find({space: {$in: spaceIds}}).sort('name').lean().exec();
+        }
+
+        /**
+         * Remove caches.
+         *
+         * @param {Array.<String>|String} ids - The cache ids for remove.
+         * @returns {Promise.<{rowsAffected}>} - The number of affected rows.
+         */
+        static remove(ids) {
+            if (_.isNil(ids))
+                return Promise.reject(new errors.IllegalArgumentException('Cache id can not be undefined or null'));
+
+            ids = _.castArray(ids);
+
+            if (_.isEmpty(ids))
+                return Promise.resolve({rowsAffected: 0});
+
+            return mongo.Cluster.updateMany({caches: {$in: ids}}, {$pull: {caches: {$in: ids}}}).exec()
+                .then(() => mongo.Cluster.updateMany({}, {$pull: {checkpointSpi: {kind: 'Cache', Cache: {cache: {$in: ids}}}}}).exec())
+                // TODO WC-201 fix cleanup of cache on deletion for cluster service configuration.
+                // .then(() => mongo.Cluster.updateMany({'serviceConfigurations.cache': cacheId}, {$unset: {'serviceConfigurations.$.cache': ''}}).exec())
+                .then(() => mongo.DomainModel.updateMany({caches: {$in: ids}}, {$pull: {caches: {$in: ids}}}).exec())
+                .then(() => mongo.Cache.deleteMany({_id: {$in: ids}}).exec())
+                .then(convertRemoveStatus);
+        }
+
+        /**
+         * Remove all caches by user.
+         *
+         * @param {mongo.ObjectId|String} userId - The user id that own caches.
+         * @param {Boolean} demo - The flag indicates that need lookup in demo space.
+         * @returns {Promise.<{rowsAffected}>} - The number of affected rows.
+         */
+        static removeAll(userId, demo) {
+            return spacesService.spaceIds(userId, demo)
+                .then(removeAllBySpaces)
+                .then(convertRemoveStatus);
+        }
+    }
+
+    return CachesService;
+};
diff --git a/modules/backend/services/clusters.js b/modules/backend/services/clusters.js
new file mode 100644
index 0000000..cd9a02b
--- /dev/null
+++ b/modules/backend/services/clusters.js
@@ -0,0 +1,279 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const _ = require('lodash');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/clusters',
+    inject: ['mongo', 'services/spaces', 'services/caches', 'services/domains', 'services/igfss', 'errors']
+};
+
+/**
+ * @param mongo
+ * @param {SpacesService} spacesService
+ * @param {CachesService} cachesService
+ * @param {DomainsService} modelsService
+ * @param {IgfssService} igfssService
+ * @param errors
+ * @returns {ClustersService}
+ */
+module.exports.factory = (mongo, spacesService, cachesService, modelsService, igfssService, errors) => {
+    /**
+     * Convert remove status operation to own presentation.
+     *
+     * @param {RemoveResult} result - The results of remove operation.
+     */
+    const convertRemoveStatus = (result) => ({rowsAffected: result.n});
+
+    /**
+     * Update existing cluster.
+     *
+     * @param {Object} cluster - The cluster for updating.
+     * @returns {Promise.<mongo.ObjectId>} that resolves cluster id.
+     */
+    const update = (cluster) => {
+        const clusterId = cluster._id;
+
+        return mongo.Cluster.updateOne({_id: clusterId}, cluster, {upsert: true}).exec()
+            .then(() => mongo.Cache.updateMany({_id: {$in: cluster.caches}}, {$addToSet: {clusters: clusterId}}).exec())
+            .then(() => mongo.Cache.updateMany({_id: {$nin: cluster.caches}}, {$pull: {clusters: clusterId}}).exec())
+            .then(() => mongo.Igfs.updateMany({_id: {$in: cluster.igfss}}, {$addToSet: {clusters: clusterId}}).exec())
+            .then(() => mongo.Igfs.updateMany({_id: {$nin: cluster.igfss}}, {$pull: {clusters: clusterId}}).exec())
+            .then(() => cluster)
+            .catch((err) => {
+                if (err.code === mongo.errCodes.DUPLICATE_KEY_UPDATE_ERROR || err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                    throw new errors.DuplicateKeyException('Cluster with name: "' + cluster.name + '" already exist.');
+                else
+                    throw err;
+            });
+    };
+
+    /**
+     * Create new cluster.
+     *
+     * @param {Object} cluster - The cluster for creation.
+     * @returns {Promise.<mongo.ObjectId>} that resolves cluster id.
+     */
+    const create = (cluster) => {
+        return mongo.Cluster.create(cluster)
+            .catch((err) => {
+                if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                    throw new errors.DuplicateKeyException(`Cluster with name: "${cluster.name}" already exist.`);
+                else
+                    throw err;
+            })
+            .then((savedCluster) =>
+                mongo.Cache.updateMany({_id: {$in: savedCluster.caches}}, {$addToSet: {clusters: savedCluster._id}}).exec()
+                    .then(() => mongo.Igfs.updateMany({_id: {$in: savedCluster.igfss}}, {$addToSet: {clusters: savedCluster._id}}).exec())
+                    .then(() => savedCluster)
+            );
+    };
+
+    /**
+     * Remove all caches by space ids.
+     *
+     * @param {Number[]} spaceIds - The space ids for cache deletion.
+     * @returns {Promise.<RemoveResult>} - that resolves results of remove operation.
+     */
+    const removeAllBySpaces = (spaceIds) => {
+        return Promise.all([
+            mongo.DomainModel.deleteMany({space: {$in: spaceIds}}).exec(),
+            mongo.Cache.deleteMany({space: {$in: spaceIds}}).exec(),
+            mongo.Igfs.deleteMany({space: {$in: spaceIds}}).exec()
+        ])
+            .then(() => mongo.Cluster.deleteMany({space: {$in: spaceIds}}).exec());
+    };
+
+    class ClustersService {
+        static shortList(userId, demo) {
+            return spacesService.spaceIds(userId, demo)
+                .then((spaceIds) => mongo.Cluster.find({space: {$in: spaceIds}}).select('name discovery.kind caches models igfss').lean().exec())
+                .then((clusters) => _.map(clusters, (cluster) => ({
+                    _id: cluster._id,
+                    name: cluster.name,
+                    discovery: cluster.discovery.kind,
+                    cachesCount: _.size(cluster.caches),
+                    modelsCount: _.size(cluster.models),
+                    igfsCount: _.size(cluster.igfss)
+                })));
+        }
+
+        static get(userId, demo, _id) {
+            return spacesService.spaceIds(userId, demo)
+                .then((spaceIds) => mongo.Cluster.findOne({space: {$in: spaceIds}, _id}).lean().exec());
+        }
+
+        static normalize(spaceId, cluster, ...models) {
+            cluster.space = spaceId;
+
+            _.forEach(models, (model) => {
+                _.forEach(model, (item) => {
+                    item.space = spaceId;
+                    item.clusters = [cluster._id];
+                });
+            });
+        }
+
+        static removedInCluster(oldCluster, newCluster, field) {
+            return _.difference(_.invokeMap(_.get(oldCluster, field), 'toString'), _.get(newCluster, field));
+        }
+
+        static upsertBasic(userId, demo, {cluster, caches}) {
+            if (_.isNil(cluster._id))
+                return Promise.reject(new errors.IllegalArgumentException('Cluster id can not be undefined or null'));
+
+            return spacesService.spaceIds(userId, demo)
+                .then((spaceIds) => {
+                    this.normalize(_.head(spaceIds), cluster, caches);
+
+                    const query = _.pick(cluster, ['space', '_id']);
+                    const basicCluster = _.pick(cluster, [
+                        'space',
+                        '_id',
+                        'name',
+                        'discovery',
+                        'caches',
+                        'memoryConfiguration.memoryPolicies',
+                        'dataStorageConfiguration.defaultDataRegionConfiguration.maxSize'
+                    ]);
+
+                    return mongo.Cluster.findOneAndUpdate(query, {$set: basicCluster}, {projection: 'caches', upsert: true}).lean().exec()
+                        .catch((err) => {
+                            if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                                throw new errors.DuplicateKeyException(`Cluster with name: "${cluster.name}" already exist.`);
+
+                            throw err;
+                        })
+                        .then((oldCluster) => {
+                            if (oldCluster) {
+                                const ids = this.removedInCluster(oldCluster, cluster, 'caches');
+
+                                return cachesService.remove(ids);
+                            }
+
+                            cluster.caches = _.map(caches, '_id');
+
+                            return mongo.Cluster.updateOne(query, {$set: cluster, new: true}, {upsert: true}).exec();
+                        });
+                })
+                .then(() => _.map(caches, cachesService.upsertBasic))
+                .then(() => ({rowsAffected: 1}));
+        }
+
+        static upsert(userId, demo, {cluster, caches, models, igfss}) {
+            if (_.isNil(cluster._id))
+                return Promise.reject(new errors.IllegalArgumentException('Cluster id can not be undefined or null'));
+
+            return spacesService.spaceIds(userId, demo)
+                .then((spaceIds) => {
+                    this.normalize(_.head(spaceIds), cluster, caches, models, igfss);
+
+                    const query = _.pick(cluster, ['space', '_id']);
+
+                    return mongo.Cluster.findOneAndUpdate(query, {$set: cluster}, {projection: {models: 1, caches: 1, igfss: 1}, upsert: true}).lean().exec()
+                        .catch((err) => {
+                            if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                                throw new errors.DuplicateKeyException(`Cluster with name: "${cluster.name}" already exist.`);
+
+                            throw err;
+                        })
+                        .then((oldCluster) => {
+                            const modelIds = this.removedInCluster(oldCluster, cluster, 'models');
+                            const cacheIds = this.removedInCluster(oldCluster, cluster, 'caches');
+                            const igfsIds = this.removedInCluster(oldCluster, cluster, 'igfss');
+
+                            return Promise.all([modelsService.remove(modelIds), cachesService.remove(cacheIds), igfssService.remove(igfsIds)]);
+                        });
+                })
+                .then(() => Promise.all(_.concat(_.map(models, modelsService.upsert), _.map(caches, cachesService.upsert), _.map(igfss, igfssService.upsert))))
+                .then(() => ({rowsAffected: 1}));
+        }
+
+        /**
+         * Create or update cluster.
+         *
+         * @param {Object} cluster - The cluster.
+         * @returns {Promise.<mongo.ObjectId>} that resolves cluster id of merge operation.
+         */
+        static merge(cluster) {
+            if (cluster._id)
+                return update(cluster);
+
+            return create(cluster);
+        }
+
+        /**
+         * Get clusters and linked objects by space.
+         *
+         * @param {mongo.ObjectId|String} spaceIds The spaces ids that own clusters.
+         * @returns {Promise.<Array<mongo.Cluster>>} Requested clusters.
+         */
+        static listBySpaces(spaceIds) {
+            return mongo.Cluster.find({space: {$in: spaceIds}}).sort('name').lean().exec();
+        }
+
+        /**
+         * Remove clusters.
+         *
+         * @param {Array.<String>|String} ids - The cluster ids for remove.
+         * @returns {Promise.<{rowsAffected}>} - The number of affected rows.
+         */
+        static remove(ids) {
+            if (_.isNil(ids))
+                return Promise.reject(new errors.IllegalArgumentException('Cluster id can not be undefined or null'));
+
+            if (_.isEmpty(ids))
+                return Promise.resolve({rowsAffected: 0});
+
+            ids = _.castArray(ids);
+
+            return Promise.all(_.map(ids, (id) => {
+                return mongo.Cluster.findByIdAndRemove(id).exec()
+                    .then((cluster) => {
+                        if (_.isNil(cluster))
+                            return 0;
+
+                        return Promise.all([
+                            mongo.DomainModel.deleteMany({_id: {$in: cluster.models}}).exec(),
+                            mongo.Cache.deleteMany({_id: {$in: cluster.caches}}).exec(),
+                            mongo.Igfs.deleteMany({_id: {$in: cluster.igfss}}).exec()
+                        ])
+                            .then(() => 1);
+                    });
+            }))
+                .then((res) => ({rowsAffected: _.sum(res)}));
+        }
+
+        /**
+         * Remove all clusters by user.
+         * @param {mongo.ObjectId|String} userId - The user id that own cluster.
+         * @param {Boolean} demo - The flag indicates that need lookup in demo space.
+         * @returns {Promise.<{rowsAffected}>} - The number of affected rows.
+         */
+        static removeAll(userId, demo) {
+            return spacesService.spaceIds(userId, demo)
+                .then(removeAllBySpaces)
+                .then(convertRemoveStatus);
+        }
+    }
+
+    return ClustersService;
+};
diff --git a/modules/backend/services/configurations.js b/modules/backend/services/configurations.js
new file mode 100644
index 0000000..da431c3
--- /dev/null
+++ b/modules/backend/services/configurations.js
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/configurations',
+    inject: ['mongo', 'services/spaces', 'services/clusters', 'services/caches', 'services/domains', 'services/igfss']
+};
+
+/**
+ * @param mongo
+ * @param {SpacesService} spacesService
+ * @param {ClustersService} clustersService
+ * @param {CachesService} cachesService
+ * @param {DomainsService} domainsService
+ * @param {IgfssService} igfssService
+ * @returns {ConfigurationsService}
+ */
+module.exports.factory = (mongo, spacesService, clustersService, cachesService, domainsService, igfssService) => {
+    class ConfigurationsService {
+        static list(userId, demo) {
+            let spaces;
+
+            return spacesService.spaces(userId, demo)
+                .then((_spaces) => {
+                    spaces = _spaces;
+
+                    return spaces.map((space) => space._id);
+                })
+                .then((spaceIds) => Promise.all([
+                    clustersService.listBySpaces(spaceIds),
+                    domainsService.listBySpaces(spaceIds),
+                    cachesService.listBySpaces(spaceIds),
+                    igfssService.listBySpaces(spaceIds)
+                ]))
+                .then(([clusters, domains, caches, igfss]) => ({clusters, domains, caches, igfss, spaces}));
+        }
+
+        static get(userId, demo, _id) {
+            return clustersService.get(userId, demo, _id)
+                .then((cluster) =>
+                    Promise.all([
+                        mongo.Cache.find({space: cluster.space, _id: {$in: cluster.caches}}).lean().exec(),
+                        mongo.DomainModel.find({space: cluster.space, _id: {$in: cluster.models}}).lean().exec(),
+                        mongo.Igfs.find({space: cluster.space, _id: {$in: cluster.igfss}}).lean().exec()
+                    ])
+                        .then(([caches, models, igfss]) => ({cluster, caches, models, igfss}))
+                );
+        }
+    }
+
+    return ConfigurationsService;
+};
diff --git a/modules/backend/services/domains.js b/modules/backend/services/domains.js
new file mode 100644
index 0000000..0d40fed
--- /dev/null
+++ b/modules/backend/services/domains.js
@@ -0,0 +1,266 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const _ = require('lodash');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/domains',
+    inject: ['mongo', 'services/spaces', 'services/caches', 'errors']
+};
+
+/**
+ * @param mongo
+ * @param {SpacesService} spacesService
+ * @param {CachesService} cachesService
+ * @param errors
+ * @returns {DomainsService}
+ */
+module.exports.factory = (mongo, spacesService, cachesService, errors) => {
+    /**
+     * Convert remove status operation to own presentation.
+     *
+     * @param {RemoveResult} result - The results of remove operation.
+     */
+    const convertRemoveStatus = (result) => ({rowsAffected: result.n});
+
+    const _updateCacheStore = (cacheStoreChanges) =>
+        Promise.all(_.map(cacheStoreChanges, (change) => mongo.Cache.updateOne({_id: {$eq: change.cacheId}}, change.change, {}).exec()));
+
+    /**
+     * Update existing domain.
+     *
+     * @param {Object} domain - The domain for updating
+     * @param savedDomains List of saved domains.
+     * @returns {Promise.<mongo.ObjectId>} that resolves domain id
+     */
+    const update = (domain, savedDomains) => {
+        const domainId = domain._id;
+
+        return mongo.DomainModel.updateOne({_id: domainId}, domain, {upsert: true}).exec()
+            .then(() => mongo.Cache.updateMany({_id: {$in: domain.caches}}, {$addToSet: {domains: domainId}}).exec())
+            .then(() => mongo.Cache.updateMany({_id: {$nin: domain.caches}}, {$pull: {domains: domainId}}).exec())
+            .then(() => {
+                savedDomains.push(domain);
+
+                return _updateCacheStore(domain.cacheStoreChanges);
+            })
+            .catch((err) => {
+                if (err.code === mongo.errCodes.DUPLICATE_KEY_UPDATE_ERROR || err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                    throw new errors.DuplicateKeyException('Domain model with value type: "' + domain.valueType + '" already exist.');
+                else
+                    throw err;
+            });
+    };
+
+    /**
+     * Create new domain.
+     *
+     * @param {Object} domain - The domain for creation.
+     * @param savedDomains List of saved domains.
+     * @returns {Promise.<mongo.ObjectId>} that resolves cluster id.
+     */
+    const create = (domain, savedDomains) => {
+        return mongo.DomainModel.create(domain)
+            .then((createdDomain) => {
+                savedDomains.push(createdDomain);
+
+                return mongo.Cache.updateMany({_id: {$in: domain.caches}}, {$addToSet: {domains: createdDomain._id}}).exec()
+                    .then(() => _updateCacheStore(domain.cacheStoreChanges));
+            })
+            .catch((err) => {
+                if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                    throw new errors.DuplicateKeyException('Domain model with value type: "' + domain.valueType + '" already exist.');
+                else
+                    throw err;
+            });
+    };
+
+    const _saveDomainModel = (domain, savedDomains) => {
+        const domainId = domain._id;
+
+        if (domainId)
+            return update(domain, savedDomains);
+
+        return create(domain, savedDomains);
+    };
+
+    const _save = (domains) => {
+        if (_.isEmpty(domains))
+            throw new errors.IllegalArgumentException('Nothing to save!');
+
+        const savedDomains = [];
+        const generatedCaches = [];
+
+        const promises = _.map(domains, (domain) => {
+            if (domain.newCache) {
+                return mongo.Cache.findOne({space: domain.space, name: domain.newCache.name}).exec()
+                    .then((cache) => {
+                        if (cache)
+                            return Promise.resolve(cache);
+
+                        // If cache not found, then create it and associate with domain model.
+                        const newCache = domain.newCache;
+                        newCache.space = domain.space;
+
+                        return cachesService.merge(newCache);
+                    })
+                    .then((cache) => {
+                        domain.caches = [cache._id];
+
+                        generatedCaches.push(cache);
+
+                        return _saveDomainModel(domain, savedDomains);
+                    });
+            }
+
+            return _saveDomainModel(domain, savedDomains);
+        });
+
+        return Promise.all(promises).then(() => ({savedDomains, generatedCaches}));
+    };
+
+    /**
+     * Remove all caches by space ids.
+     *
+     * @param {Array.<Number>} spaceIds - The space ids for cache deletion.
+     * @returns {Promise.<RemoveResult>} - that resolves results of remove operation.
+     */
+    const removeAllBySpaces = (spaceIds) => {
+        return mongo.Cache.updateMany({space: {$in: spaceIds}}, {domains: []}).exec()
+            .then(() => mongo.DomainModel.deleteMany({space: {$in: spaceIds}}).exec());
+    };
+
+    class DomainsService {
+        static shortList(userId, demo, clusterId) {
+            return spacesService.spaceIds(userId, demo)
+                .then((spaceIds) => {
+                    const sIds = _.map(spaceIds, (spaceId) => mongo.ObjectId(spaceId));
+
+                    return mongo.DomainModel.aggregate([
+                        {$match: {space: {$in: sIds}, clusters: mongo.ObjectId(clusterId)}},
+                        {$project: {
+                            keyType: 1,
+                            valueType: 1,
+                            queryMetadata: 1,
+                            hasIndex: {
+                                $or: [
+                                    {
+                                        $and: [
+                                            {$eq: ['$queryMetadata', 'Annotations']},
+                                            {
+                                                $or: [
+                                                    {$eq: ['$generatePojo', false]},
+                                                    {
+                                                        $and: [
+                                                            {$eq: ['$databaseSchema', '']},
+                                                            {$eq: ['$databaseTable', '']}
+                                                        ]
+                                                    }
+                                                ]
+                                            }
+                                        ]
+                                    },
+                                    {$gt: [{$size: {$ifNull: ['$keyFields', []]}}, 0]}
+                                ]
+                            }
+                        }}
+                    ]).exec();
+                });
+        }
+
+        static get(userId, demo, _id) {
+            return spacesService.spaceIds(userId, demo)
+                .then((spaceIds) => mongo.DomainModel.findOne({space: {$in: spaceIds}, _id}).lean().exec());
+        }
+
+        static upsert(model) {
+            if (_.isNil(model._id))
+                return Promise.reject(new errors.IllegalArgumentException('Model id can not be undefined or null'));
+
+            const query = _.pick(model, ['space', '_id']);
+
+            return mongo.DomainModel.updateOne(query, {$set: model}, {upsert: true}).exec()
+                .then(() => mongo.Cache.updateMany({_id: {$in: model.caches}}, {$addToSet: {domains: model._id}}).exec())
+                .then(() => mongo.Cache.updateMany({_id: {$nin: model.caches}}, {$pull: {domains: model._id}}).exec())
+                .then(() => _updateCacheStore(model.cacheStoreChanges))
+                .catch((err) => {
+                    if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                        throw new errors.DuplicateKeyException(`Model with value type: "${model.valueType}" already exist.`);
+
+                    throw err;
+                });
+        }
+
+        /**
+         * Remove model.
+         *
+         * @param {mongo.ObjectId|String} ids - The model id for remove.
+         * @returns {Promise.<{rowsAffected}>} - The number of affected rows.
+         */
+        static remove(ids) {
+            if (_.isNil(ids))
+                return Promise.reject(new errors.IllegalArgumentException('Model id can not be undefined or null'));
+
+            ids = _.castArray(ids);
+
+            if (_.isEmpty(ids))
+                return Promise.resolve({rowsAffected: 0});
+
+            return mongo.Cache.updateMany({domains: {$in: ids}}, {$pull: {domains: ids}}).exec()
+                .then(() => mongo.Cluster.updateMany({models: {$in: ids}}, {$pull: {models: ids}}).exec())
+                .then(() => mongo.DomainModel.deleteMany({_id: {$in: ids}}).exec())
+                .then(convertRemoveStatus);
+        }
+
+        /**
+         * Batch merging domains.
+         *
+         * @param {Array.<mongo.DomainModel>} domains
+         */
+        static batchMerge(domains) {
+            return _save(domains);
+        }
+
+        /**
+         * Get domain and linked objects by space.
+         *
+         * @param {mongo.ObjectId|String} spaceIds - The space id that own domain.
+         * @returns {Promise.<Array.<mongo.DomainModel>>} contains requested domains.
+         */
+        static listBySpaces(spaceIds) {
+            return mongo.DomainModel.find({space: {$in: spaceIds}}).sort('valueType').lean().exec();
+        }
+
+        /**
+         * Remove all domains by user.
+         * @param {mongo.ObjectId|String} userId - The user id that own domain.
+         * @param {Boolean} demo - The flag indicates that need lookup in demo space.
+         * @returns {Promise.<{rowsAffected}>} - The number of affected rows.
+         */
+        static removeAll(userId, demo) {
+            return spacesService.spaceIds(userId, demo)
+                .then(removeAllBySpaces)
+                .then(convertRemoveStatus);
+        }
+    }
+
+    return DomainsService;
+};
diff --git a/modules/backend/services/downloads.js b/modules/backend/services/downloads.js
new file mode 100644
index 0000000..75548c8
--- /dev/null
+++ b/modules/backend/services/downloads.js
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const _ = require('lodash');
+const JSZip = require('jszip');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/agents',
+    inject: ['settings', 'agents-handler', 'errors']
+};
+
+/**
+ * @param settings
+ * @param agentsHnd
+ * @param errors
+ * @returns {DownloadsService}
+ */
+module.exports.factory = (settings, agentsHnd, errors) => {
+    class DownloadsService {
+        /**
+         * Get agent archive with user agent configuration.
+         *
+         * @returns {*} - readable stream for further piping. (http://stuk.github.io/jszip/documentation/api_jszip/generate_node_stream.html)
+         */
+        prepareArchive(host, token) {
+            if (_.isEmpty(agentsHnd.currentAgent))
+                throw new errors.MissingResourceException('Missing agent zip on server. Please ask webmaster to upload agent zip!');
+
+            const {filePath, fileName} = agentsHnd.currentAgent;
+
+            const folder = path.basename(fileName, '.zip');
+
+            // Read a zip file.
+            return new Promise((resolve, reject) => {
+                fs.readFile(filePath, (errFs, data) => {
+                    if (errFs)
+                        reject(new errors.ServerErrorException(errFs));
+
+                    JSZip.loadAsync(data)
+                        .then((zip) => {
+                            const prop = [];
+
+                            prop.push(`tokens=${token}`);
+                            prop.push(`server-uri=${host}`);
+                            prop.push('#Uncomment following options if needed:');
+                            prop.push('#node-uri=http://localhost:8080');
+                            prop.push('#node-login=ignite');
+                            prop.push('#node-password=ignite');
+                            prop.push('#driver-folder=./jdbc-drivers');
+                            prop.push('#Uncomment and configure following SSL options if needed:');
+                            prop.push('#node-key-store=client.jks');
+                            prop.push('#node-key-store-password=MY_PASSWORD');
+                            prop.push('#node-trust-store=ca.jks');
+                            prop.push('#node-trust-store-password=MY_PASSWORD');
+                            prop.push('#server-key-store=client.jks');
+                            prop.push('#server-key-store-password=MY_PASSWORD');
+                            prop.push('#server-trust-store=ca.jks');
+                            prop.push('#server-trust-store-password=MY_PASSWORD');
+                            prop.push('#cipher-suites=CIPHER1,CIPHER2,CIPHER3');
+
+                            zip.file(`${folder}/default.properties`, prop.join('\n'));
+
+                            return zip.generateAsync({type: 'nodebuffer', platform: 'UNIX'})
+                                .then((buffer) => resolve({filePath, fileName, buffer}));
+                        })
+                        .catch(reject);
+                });
+            });
+        }
+    }
+
+    return new DownloadsService();
+};
diff --git a/modules/backend/services/igfss.js b/modules/backend/services/igfss.js
new file mode 100644
index 0000000..52440b7
--- /dev/null
+++ b/modules/backend/services/igfss.js
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const _ = require('lodash');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/igfss',
+    inject: ['mongo', 'services/spaces', 'errors']
+};
+
+/**
+ * @param mongo
+ * @param {SpacesService} spacesService
+ * @param errors
+ * @returns {IgfssService}
+ */
+module.exports.factory = (mongo, spacesService, errors) => {
+    /**
+     * Convert remove status operation to own presentation.
+     *
+     * @param {RemoveResult} result - The results of remove operation.
+     */
+    const convertRemoveStatus = (result) => ({rowsAffected: result.n});
+
+    /**
+     * Update existing IGFS.
+     *
+     * @param {Object} igfs - The IGFS for updating
+     * @returns {Promise.<mongo.ObjectId>} that resolves IGFS id
+     */
+    const update = (igfs) => {
+        const igfsId = igfs._id;
+
+        return mongo.Igfs.updateOne({_id: igfsId}, igfs, {upsert: true}).exec()
+            .then(() => mongo.Cluster.updateMany({_id: {$in: igfs.clusters}}, {$addToSet: {igfss: igfsId}}).exec())
+            .then(() => mongo.Cluster.updateMany({_id: {$nin: igfs.clusters}}, {$pull: {igfss: igfsId}}).exec())
+            .then(() => igfs)
+            .catch((err) => {
+                if (err.code === mongo.errCodes.DUPLICATE_KEY_UPDATE_ERROR || err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                    throw new errors.DuplicateKeyException('IGFS with name: "' + igfs.name + '" already exist.');
+                else
+                    throw err;
+            });
+    };
+
+    /**
+     * Create new IGFS.
+     *
+     * @param {Object} igfs - The IGFS for creation.
+     * @returns {Promise.<mongo.ObjectId>} that resolves IGFS id.
+     */
+    const create = (igfs) => {
+        return mongo.Igfs.create(igfs)
+            .then((savedIgfs) =>
+                mongo.Cluster.updateMany({_id: {$in: savedIgfs.clusters}}, {$addToSet: {igfss: savedIgfs._id}}).exec()
+                    .then(() => savedIgfs)
+            )
+            .catch((err) => {
+                if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                    throw new errors.DuplicateKeyException('IGFS with name: "' + igfs.name + '" already exist.');
+                else
+                    throw err;
+            });
+    };
+
+    /**
+     * Remove all IGFSs by space ids.
+     *
+     * @param {Number[]} spaceIds - The space ids for IGFS deletion.
+     * @returns {Promise.<RemoveResult>} - that resolves results of remove operation.
+     */
+    const removeAllBySpaces = (spaceIds) => {
+        return mongo.Cluster.updateMany({space: {$in: spaceIds}}, {igfss: []}).exec()
+            .then(() => mongo.Igfs.deleteMany({space: {$in: spaceIds}}).exec());
+    };
+
+    class IgfssService {
+        static shortList(userId, demo, clusterId) {
+            return spacesService.spaceIds(userId, demo)
+                .then((spaceIds) => mongo.Igfs.find({space: {$in: spaceIds}, clusters: clusterId }).select('name defaultMode affinnityGroupSize').lean().exec());
+        }
+
+        static get(userId, demo, _id) {
+            return spacesService.spaceIds(userId, demo)
+                .then((spaceIds) => mongo.Igfs.findOne({space: {$in: spaceIds}, _id}).lean().exec());
+        }
+
+        static upsert(igfs) {
+            if (_.isNil(igfs._id))
+                return Promise.reject(new errors.IllegalArgumentException('IGFS id can not be undefined or null'));
+
+            const query = _.pick(igfs, ['space', '_id']);
+
+            return mongo.Igfs.updateOne(query, {$set: igfs}, {upsert: true}).exec()
+                .catch((err) => {
+                    if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                        throw new errors.DuplicateKeyException(`IGFS with name: "${igfs.name}" already exist.`);
+
+                    throw err;
+                });
+        }
+
+        /**
+         * Create or update IGFS.
+         *
+         * @param {Object} igfs - The IGFS
+         * @returns {Promise.<mongo.ObjectId>} that resolves IGFS id of merge operation.
+         */
+        static merge(igfs) {
+            if (igfs._id)
+                return update(igfs);
+
+            return create(igfs);
+        }
+
+        /**
+         * Get IGFS by spaces.
+         *
+         * @param {mongo.ObjectId|String} spacesIds - The spaces ids that own IGFSs.
+         * @returns {Promise.<Array<mongo.IGFS>>} - contains requested IGFSs.
+         */
+        static listBySpaces(spacesIds) {
+            return mongo.Igfs.find({space: {$in: spacesIds}}).sort('name').lean().exec();
+        }
+
+        /**
+         * Remove IGFSs.
+         *
+         * @param {Array.<String>|String} ids - The IGFS ids for remove.
+         * @returns {Promise.<{rowsAffected}>} - The number of affected rows.
+         */
+        static remove(ids) {
+            if (_.isNil(ids))
+                return Promise.reject(new errors.IllegalArgumentException('IGFS id can not be undefined or null'));
+
+            ids = _.castArray(ids);
+
+            if (_.isEmpty(ids))
+                return Promise.resolve({rowsAffected: 0});
+
+            return mongo.Cluster.updateMany({igfss: {$in: ids}}, {$pull: {igfss: {$in: ids}}}).exec()
+                // TODO WC-201 fix cleanup on node filter on deletion for cluster serviceConfigurations and caches.
+                // .then(() => mongo.Cluster.updateMany({ 'serviceConfigurations.$.nodeFilter.kind': { $ne: 'IGFS' }, 'serviceConfigurations.nodeFilter.IGFS.igfs': igfsId},
+                //     {$unset: {'serviceConfigurations.$.nodeFilter.IGFS.igfs': ''}}).exec())
+                // .then(() => mongo.Cluster.updateMany({ 'serviceConfigurations.nodeFilter.kind': 'IGFS', 'serviceConfigurations.nodeFilter.IGFS.igfs': igfsId},
+                //     {$unset: {'serviceConfigurations.$.nodeFilter': ''}}).exec())
+                .then(() => mongo.Igfs.deleteMany({_id: {$in: ids}}).exec())
+                .then(convertRemoveStatus);
+        }
+
+        /**
+         * Remove all IGFSes by user.
+         *
+         * @param {mongo.ObjectId|String} userId - The user id that own IGFS.
+         * @param {Boolean} demo - The flag indicates that need lookup in demo space.
+         * @returns {Promise.<{rowsAffected}>} - The number of affected rows.
+         */
+        static removeAll(userId, demo) {
+            return spacesService.spaceIds(userId, demo)
+                .then(removeAllBySpaces)
+                .then(convertRemoveStatus);
+        }
+    }
+
+    return IgfssService;
+};
diff --git a/modules/backend/services/mails.js b/modules/backend/services/mails.js
new file mode 100644
index 0000000..c51704f
--- /dev/null
+++ b/modules/backend/services/mails.js
@@ -0,0 +1,216 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const fs = require('fs');
+const _ = require('lodash');
+const nodemailer = require('nodemailer');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/mails',
+    inject: ['settings']
+};
+
+/**
+ * @param settings
+ * @returns {MailsService}
+ */
+module.exports.factory = (settings) => {
+    class MailsService {
+        /**
+         * Read template file.
+         * @param {String} template Path to template file.
+         * @param template
+         */
+        readTemplate(template) {
+            try {
+                return fs.readFileSync(template, 'utf8');
+            }
+            catch (ignored) {
+                throw new Error('Failed to find email template: ' + template);
+            }
+        }
+
+        /**
+         * Get message with resolved variables.
+         *
+         * @param {string} template Message template.
+         * @param {object} ctx Context.
+         * @return Prepared template.
+         * @throws IOException If failed to prepare template.
+         */
+        getMessage(template, ctx) {
+            _.forIn(ctx, (value, key) => template = template.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), value || 'n/a'));
+
+            return template;
+        }
+
+        /**
+         * @param {string} host Web Console host.
+         * @param {Account} user User that signed up.
+         * @param {string} message Message.
+         * @param {object} customCtx Custom context parameters.
+         */
+        buildContext(host, user, message, customCtx) {
+            return {
+                message,
+                ...customCtx,
+                greeting: settings.mail.greeting,
+                sign: settings.mail.sign,
+                firstName: user.firstName,
+                lastName: user.lastName,
+                email: user.res,
+                host,
+                activationLink: `${host}/signin?activationToken=${user.activationToken}`,
+                resetLink: `${host}/password/reset?token=${user.resetPasswordToken}`
+            };
+        }
+
+        /**
+         * Send mail to user.
+         *
+         * @param {string} template Path to template file.
+         * @param {string} host Web Console host.
+         * @param {Account} user User that signed up.
+         * @param {string} subject Email subject.
+         * @param {string} message Email message.
+         * @param {object} customCtx Custom context parameters.
+         * @throws {Error}
+         * @return {Promise}
+         */
+        send(template, host, user, subject, message, customCtx = {}) {
+            const options = settings.mail;
+
+            return new Promise((resolve, reject) => {
+                if (_.isEmpty(options))
+                    reject(new Error('SMTP server is not configured.'));
+
+                if (!_.isEmpty(options.service)) {
+                    if (_.isEmpty(options.auth) || _.isEmpty(options.auth.user) || _.isEmpty(options.auth.pass))
+                        reject(new Error(`Credentials is not configured for service: ${options.service}`));
+                }
+
+                resolve(nodemailer.createTransport(options));
+            })
+                .then((transporter) => {
+                    return transporter.verify().then(() => transporter);
+                })
+                .then((transporter) => {
+                    const context = this.buildContext(host, user, message, customCtx);
+
+                    context.subject = this.getMessage(subject, context);
+
+                    return transporter.sendMail({
+                        from: options.from,
+                        to: `"${user.firstName} ${user.lastName}" <${user.email}>`,
+                        subject: context.subject,
+                        html: this.getMessage(this.readTemplate(template), context)
+                    });
+                })
+                .catch((err) => {
+                    console.log('Failed to send email.', err);
+
+                    return Promise.reject(err);
+                });
+        }
+
+        /**
+         * Send email when user signed up.
+         *
+         * @param host Web Console host.
+         * @param user User that signed up.
+         * @param createdByAdmin Whether user was created by admin.
+         */
+        sendWelcomeLetter(host, user, createdByAdmin) {
+            if (createdByAdmin) {
+                return this.send('templates/base.html', host, user, 'Account was created for ${greeting}.',
+                    'You are receiving this email because administrator created account for you to use <a href="${host}">${greeting}</a>.<br><br>' +
+                    'If you do not know what this email is about, please ignore it.<br>' +
+                    'You may reset the password by clicking on the following link, or paste this into your browser:<br><br>' +
+                    '<a href="${resetLink}">${resetLink}</a>'
+                );
+            }
+
+            return this.send('templates/base.html', host, user, 'Thanks for signing up for ${greeting}.',
+                'You are receiving this email because you have signed up to use <a href="${host}">${greeting}</a>.<br><br>' +
+                'If you do not know what this email is about, please ignore it.<br>' +
+                'You may reset the password by clicking on the following link, or paste this into your browser:<br><br>' +
+                '<a href="${resetLink}">${resetLink}</a>'
+            );
+        }
+
+        /**
+         * Send email to user for password reset.
+         *
+         * @param host
+         * @param user
+         */
+        sendActivationLink(host, user) {
+            return this.send('templates/base.html', host, user, 'Confirm your account on ${greeting}',
+                'You are receiving this email because you have signed up to use <a href="${host}">${greeting}</a>.<br><br>' +
+                'Please click on the following link, or paste this into your browser to activate your account:<br><br>' +
+                '<a href="${activationLink}">${activationLink}</a>'
+            )
+                .catch(() => Promise.reject(new Error('Failed to send email with confirm account link!')));
+        }
+
+        /**
+         * Send email to user for password reset.
+         *
+         * @param host
+         * @param user
+         */
+        sendResetLink(host, user) {
+            return this.send('templates/base.html', host, user, 'Password Reset',
+                'You are receiving this because you (or someone else) have requested the reset of the password for your account.<br><br>' +
+                'Please click on the following link, or paste this into your browser to complete the process:<br><br>' +
+                '<a href="${resetLink}">${resetLink}</a><br><br>' +
+                'If you did not request this, please ignore this email and your password will remain unchanged.'
+            )
+                .catch(() => Promise.reject(new Error('Failed to send email with reset link!')));
+        }
+
+        /**
+         * Send email to user for password reset.
+         * @param host
+         * @param user
+         */
+        sendPasswordChanged(host, user) {
+            return this.send('templates/base.html', host, user, 'Your password has been changed',
+                'This is a confirmation that the password for your account on <a href="${host}">${greeting}</a> has just been changed.'
+            )
+                .catch(() => Promise.reject(new Error('Password was changed, but failed to send confirmation email!')));
+        }
+
+        /**
+         * Send email to user when it was deleted.
+         * @param host
+         * @param user
+         */
+        sendAccountDeleted(host, user) {
+            return this.send('templates/base.html', host, user, 'Your account was removed',
+                'You are receiving this email because your account for <a href="${host}">${greeting}</a> was removed.',
+                'Account was removed, but failed to send email notification to user!')
+                .catch(() => Promise.reject(new Error('Password was changed, but failed to send confirmation email!')));
+        }
+    }
+
+    return new MailsService();
+};
diff --git a/modules/backend/services/notebooks.js b/modules/backend/services/notebooks.js
new file mode 100644
index 0000000..760b036
--- /dev/null
+++ b/modules/backend/services/notebooks.js
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const _ = require('lodash');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/notebooks',
+    inject: ['mongo', 'services/spaces', 'errors']
+};
+
+/**
+ * @param mongo
+ * @param {SpacesService} spacesService
+ * @param errors
+ * @returns {NotebooksService}
+ */
+module.exports.factory = (mongo, spacesService, errors) => {
+    /**
+     * Convert remove status operation to own presentation.
+     *
+     * @param {RemoveResult} result - The results of remove operation.
+     */
+    const convertRemoveStatus = (result) => ({rowsAffected: result.n});
+
+    /**
+     * Update existing notebook.
+     *
+     * @param {Object} notebook - The notebook for updating
+     * @returns {Promise.<mongo.ObjectId>} that resolves cache id
+     */
+    const update = (notebook) => {
+        return mongo.Notebook.findOneAndUpdate({_id: notebook._id}, notebook, {new: true, upsert: true}).exec()
+            .catch((err) => {
+                if (err.code === mongo.errCodes.DUPLICATE_KEY_UPDATE_ERROR || err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                    throw new errors.DuplicateKeyException('Notebook with name: "' + notebook.name + '" already exist.');
+                else
+                    throw err;
+            });
+    };
+
+    /**
+     * Create new notebook.
+     *
+     * @param {Object} notebook - The notebook for creation.
+     * @returns {Promise.<mongo.ObjectId>} that resolves cache id.
+     */
+    const create = (notebook) => {
+        return mongo.Notebook.create(notebook)
+            .catch((err) => {
+                if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
+                    throw new errors.DuplicateKeyException('Notebook with name: "' + notebook.name + '" already exist.');
+                else
+                    throw err;
+            });
+    };
+
+    class NotebooksService {
+        /**
+         * Create or update Notebook.
+         *
+         * @param {Object} notebook - The Notebook
+         * @returns {Promise.<mongo.ObjectId>} that resolves Notebook id of merge operation.
+         */
+        static merge(notebook) {
+            if (notebook._id)
+                return update(notebook);
+
+            return create(notebook);
+        }
+
+        /**
+         * Get notebooks by spaces.
+         *
+         * @param {mongo.ObjectId|String} spaceIds - The spaces ids that own caches.
+         * @returns {Promise.<mongo.Notebook[]>} - contains requested caches.
+         */
+        static listBySpaces(spaceIds) {
+            return mongo.Notebook.find({space: {$in: spaceIds}}).sort('name').lean().exec();
+        }
+
+        /**
+         * Remove notebook.
+         *
+         * @param {mongo.ObjectId|String} notebookId - The Notebook id for remove.
+         * @returns {Promise.<{rowsAffected}>} - The number of affected rows.
+         */
+        static remove(notebookId) {
+            if (_.isNil(notebookId))
+                return Promise.reject(new errors.IllegalArgumentException('Notebook id can not be undefined or null'));
+
+            return mongo.Notebook.deleteOne({_id: notebookId}).exec()
+                .then(convertRemoveStatus);
+        }
+    }
+
+    return NotebooksService;
+};
diff --git a/modules/backend/services/notifications.js b/modules/backend/services/notifications.js
new file mode 100644
index 0000000..4dacf51
--- /dev/null
+++ b/modules/backend/services/notifications.js
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/notifications',
+    inject: ['mongo', 'browsers-handler']
+};
+
+/**
+ * @param mongo
+ * @param browsersHnd
+ * @returns {NotificationsService}
+ */
+module.exports.factory = (mongo, browsersHnd) => {
+    class NotificationsService {
+        /**
+         * Update notifications.
+         *
+         * @param {String} owner - User ID
+         * @param {String} message - Message to users.
+         * @param {Boolean} isShown - Whether to show message.
+         * @param {Date} [date] - Optional date to save in notifications.
+         * @returns {Promise.<mongo.ObjectId>} that resolve activity
+         */
+        static merge(owner, message, isShown = false, date = new Date()) {
+            return mongo.Notifications.create({owner, message, date, isShown})
+                .then(({message, date, isShown}) => browsersHnd.updateNotification({message, date, isShown}));
+        }
+    }
+
+    return NotificationsService;
+};
diff --git a/modules/backend/services/sessions.js b/modules/backend/services/sessions.js
new file mode 100644
index 0000000..0ea851b
--- /dev/null
+++ b/modules/backend/services/sessions.js
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/sessions',
+    inject: ['mongo', 'errors']
+};
+
+/**
+ * @param mongo
+ * @param errors
+ * @returns {SessionsService}
+ */
+module.exports.factory = (mongo, errors) => {
+    class SessionsService {
+        /**
+         * Become user.
+         * @param {Session} session - current session of user.
+         * @param {mongo.ObjectId|String} viewedUserId - id of user to become.
+         */
+        static become(session, viewedUserId) {
+            if (!session.req.user.admin)
+                return Promise.reject(new errors.IllegalAccessError('Became this user is not permitted. Only administrators can perform this actions.'));
+
+            return mongo.Account.findById(viewedUserId).lean().exec()
+                .then((viewedUser) => session.viewedUser = viewedUser);
+        }
+
+        /**
+         * Revert to your identity.
+         */
+        static revert(session) {
+            return new Promise((resolve) => {
+                delete session.viewedUser;
+
+                resolve(true);
+            });
+        }
+    }
+
+    return SessionsService;
+};
diff --git a/modules/backend/services/spaces.js b/modules/backend/services/spaces.js
new file mode 100644
index 0000000..41639dc
--- /dev/null
+++ b/modules/backend/services/spaces.js
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/spaces',
+    inject: ['mongo', 'errors']
+};
+
+/**
+ * @param mongo
+ * @param errors
+ * @returns {SpacesService}
+ */
+module.exports.factory = (mongo, errors) => {
+    class SpacesService {
+        /**
+         * Query for user spaces.
+         *
+         * @param {mongo.ObjectId|String} userId User ID.
+         * @param {Boolean} demo Is need use demo space.
+         * @returns {Promise}
+         */
+        static spaces(userId, demo) {
+            return mongo.Space.find({owner: userId, demo: !!demo}).lean().exec()
+                .then((spaces) => {
+                    if (!spaces.length)
+                        throw new errors.MissingResourceException('Failed to find space');
+
+                    return spaces;
+                });
+        }
+
+        /**
+         * Extract IDs from user spaces.
+         *
+         * @param {mongo.ObjectId|String} userId User ID.
+         * @param {Boolean} demo Is need use demo space.
+         * @returns {Promise}
+         */
+        static spaceIds(userId, demo) {
+            return this.spaces(userId, demo)
+                .then((spaces) => spaces.map((space) => space._id.toString()));
+        }
+
+        /**
+         * Create demo space for user
+         * @param userId - user id
+         * @returns {Promise<mongo.Space>} that resolves created demo space for user
+         */
+        static createDemoSpace(userId) {
+            return new mongo.Space({name: 'Demo space', owner: userId, demo: true}).save();
+        }
+
+        /**
+         * Clean up spaces.
+         *
+         * @param {mongo.ObjectId|String} spaceIds - The space ids for clean up.
+         * @returns {Promise.<>}
+         */
+        static cleanUp(spaceIds) {
+            return Promise.all([
+                mongo.Cluster.deleteMany({space: {$in: spaceIds}}).exec(),
+                mongo.Cache.deleteMany({space: {$in: spaceIds}}).exec(),
+                mongo.DomainModel.deleteMany({space: {$in: spaceIds}}).exec(),
+                mongo.Igfs.deleteMany({space: {$in: spaceIds}}).exec()
+            ]);
+        }
+    }
+
+    return SpacesService;
+};
+
diff --git a/modules/backend/services/users.js b/modules/backend/services/users.js
new file mode 100644
index 0000000..896c677
--- /dev/null
+++ b/modules/backend/services/users.js
@@ -0,0 +1,275 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const _ = require('lodash');
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/users',
+    inject: ['errors', 'settings', 'mongo', 'services/spaces', 'services/mails', 'services/activities', 'services/utils', 'agents-handler']
+};
+
+/**
+ * @param mongo
+ * @param errors
+ * @param settings
+ * @param {SpacesService} spacesService
+ * @param {MailsService} mailsService
+ * @param {ActivitiesService} activitiesService
+ * @param {UtilsService} utilsService
+ * @param {AgentsHandler} agentHnd
+ * @returns {UsersService}
+ */
+module.exports.factory = (errors, settings, mongo, spacesService, mailsService, activitiesService, utilsService, agentHnd) => {
+    class UsersService {
+        /**
+         * Save profile information.
+         *
+         * @param {String} host - The host.
+         * @param {Object} user - The user.
+         * @param {Object} createdByAdmin - Whether user created by admin.
+         * @returns {Promise.<mongo.ObjectId>} that resolves account id of merge operation.
+         */
+        static create(host, user, createdByAdmin) {
+            return mongo.Account.count().exec()
+                .then((cnt) => {
+                    user.admin = cnt === 0;
+                    user.registered = new Date();
+                    user.token = utilsService.randomString(settings.tokenLength);
+                    user.resetPasswordToken = utilsService.randomString(settings.tokenLength);
+                    user.activated = false;
+
+                    if (settings.activation.enabled) {
+                        user.activationToken = utilsService.randomString(settings.tokenLength);
+                        user.activationSentAt = new Date();
+                    }
+
+                    if (settings.server.disableSignup && !user.admin && !createdByAdmin)
+                        throw new errors.ServerErrorException('Sign-up is not allowed. Ask your Web Console administrator to create account for you.');
+
+                    return new mongo.Account(user);
+                })
+                .then((created) => {
+                    return new Promise((resolve, reject) => {
+                        mongo.Account.register(created, user.password, (err, registered) => {
+                            if (err)
+                                reject(err);
+
+                            if (!registered)
+                                reject(new errors.ServerErrorException('Failed to register user.'));
+
+                            resolve(registered);
+                        });
+                    });
+                })
+                .then((registered) => {
+                    return mongo.Space.create({name: 'Personal space', owner: registered._id})
+                        .then(() => registered);
+                })
+                .then((registered) => {
+                    if (settings.activation.enabled) {
+                        mailsService.sendActivationLink(host, registered);
+
+                        if (createdByAdmin)
+                            return registered;
+
+                        throw new errors.MissingConfirmRegistrationException(registered.email);
+                    }
+
+                    mailsService.sendWelcomeLetter(host, registered, createdByAdmin);
+
+                    return registered;
+                });
+        }
+
+        /**
+         * Save user.
+         *
+         * @param userId User ID.
+         * @param {Object} changed Changed user.
+         * @returns {Promise.<mongo.ObjectId>} that resolves account id of merge operation.
+         */
+        static save(userId, changed) {
+            delete changed.admin;
+            delete changed.activated;
+            delete changed.activationSentAt;
+            delete changed.activationToken;
+
+            return mongo.Account.findById(userId).exec()
+                .then((user) => {
+                    if (!changed.password)
+                        return Promise.resolve(user);
+
+                    return new Promise((resolve, reject) => {
+                        user.setPassword(changed.password, (err, _user) => {
+                            if (err)
+                                return reject(err);
+
+                            delete changed.password;
+
+                            resolve(_user);
+                        });
+                    });
+                })
+                .then((user) => {
+                    if (!changed.email || user.email === changed.email)
+                        return Promise.resolve(user);
+
+                    return new Promise((resolve, reject) => {
+                        mongo.Account.findOne({email: changed.email}, (err, _user) => {
+                            // TODO send error to admin
+                            if (err)
+                                reject(new Error('Failed to check email!'));
+
+                            if (_user && _user._id !== user._id)
+                                reject(new Error('User with this email already registered!'));
+
+                            resolve(user);
+                        });
+                    });
+                })
+                .then((user) => {
+                    if (changed.token && user.token !== changed.token)
+                        agentHnd.onTokenReset(user);
+
+                    _.extend(user, changed);
+
+                    return user.save();
+                });
+        }
+
+        /**
+         * Get list of user accounts and summary information.
+         *
+         * @returns {mongo.Account[]} - returns all accounts with counters object
+         */
+        static list(params) {
+            return Promise.all([
+                Promise.all([
+                    mongo.Account.aggregate([
+                        {$lookup: {from: 'spaces', localField: '_id', foreignField: 'owner', as: 'spaces'}},
+                        {$project: {
+                            _id: 1,
+                            firstName: 1,
+                            lastName: 1,
+                            admin: 1,
+                            email: 1,
+                            company: 1,
+                            country: 1,
+                            lastLogin: 1,
+                            lastActivity: 1,
+                            activated: 1,
+                            spaces: {
+                                $filter: {
+                                    input: '$spaces',
+                                    as: 'space',
+                                    cond: {$eq: ['$$space.demo', false]}
+                                }
+                            }
+                        }},
+                        { $sort: {firstName: 1, lastName: 1}}
+                    ]).exec(),
+                    mongo.Cluster.aggregate([{$group: {_id: '$space', count: { $sum: 1 }}}]).exec(),
+                    mongo.Cache.aggregate([{$group: {_id: '$space', count: { $sum: 1 }}}]).exec(),
+                    mongo.DomainModel.aggregate([{$group: {_id: '$space', count: { $sum: 1 }}}]).exec(),
+                    mongo.Igfs.aggregate([{$group: {_id: '$space', count: { $sum: 1 }}}]).exec()
+                ]).then(([users, clusters, caches, models, igfs]) => {
+                    const clustersMap = _.mapValues(_.keyBy(clusters, '_id'), 'count');
+                    const cachesMap = _.mapValues(_.keyBy(caches, '_id'), 'count');
+                    const modelsMap = _.mapValues(_.keyBy(models, '_id'), 'count');
+                    const igfsMap = _.mapValues(_.keyBy(igfs, '_id'), 'count');
+
+                    _.forEach(users, (user) => {
+                        const counters = user.counters = {};
+
+                        counters.clusters = _.sumBy(user.spaces, ({_id}) => clustersMap[_id]) || 0;
+                        counters.caches = _.sumBy(user.spaces, ({_id}) => cachesMap[_id]) || 0;
+                        counters.models = _.sumBy(user.spaces, ({_id}) => modelsMap[_id]) || 0;
+                        counters.igfs = _.sumBy(user.spaces, ({_id}) => igfsMap[_id]) || 0;
+
+                        delete user.spaces;
+                    });
+
+                    return users;
+                }),
+                activitiesService.total(params),
+                activitiesService.detail(params)
+            ])
+                .then(([users, activitiesTotal, activitiesDetail]) => {
+                    _.forEach(users, (user) => {
+                        user.activitiesTotal = activitiesTotal[user._id];
+                        user.activitiesDetail = activitiesDetail[user._id];
+                    });
+
+                    return users;
+                });
+        }
+
+        /**
+         * Remove account.
+         *
+         * @param {String} host.
+         * @param {mongo.ObjectId|String} userId - The account id for remove.
+         * @returns {Promise.<{rowsAffected}>} - The number of affected rows.
+         */
+        static remove(host, userId) {
+            return mongo.Account.findByIdAndRemove(userId).exec()
+                .then((user) => {
+                    return spacesService.spaceIds(userId)
+                        .then((spaceIds) => Promise.all([
+                            mongo.Cluster.deleteMany({space: {$in: spaceIds}}).exec(),
+                            mongo.Cache.deleteMany({space: {$in: spaceIds}}).exec(),
+                            mongo.DomainModel.deleteMany({space: {$in: spaceIds}}).exec(),
+                            mongo.Igfs.deleteMany({space: {$in: spaceIds}}).exec(),
+                            mongo.Notebook.deleteMany({space: {$in: spaceIds}}).exec(),
+                            mongo.Space.deleteOne({owner: userId}).exec()
+                        ]))
+                        .catch((err) => console.error(`Failed to cleanup spaces [user=${user.username}, err=${err}`))
+                        .then(() => user);
+                })
+                .then((user) => mailsService.sendAccountDeleted(host, user));
+        }
+
+        /**
+         * Get account information.
+         */
+        static get(user, viewedUser) {
+            if (_.isNil(user))
+                return Promise.reject(new errors.AuthFailedException('The user profile service failed the sign in. User profile cannot be loaded.'));
+
+            const becomeUsed = viewedUser && user.admin;
+
+            if (becomeUsed)
+                user = _.extend({}, viewedUser, {becomeUsed: true, becameToken: user.token});
+            else
+                user = user.toJSON();
+
+            return mongo.Space.findOne({owner: user._id, demo: true}).exec()
+                .then((demoSpace) => {
+                    if (user && demoSpace)
+                        user.demoCreated = true;
+
+                    return user;
+                });
+        }
+    }
+
+    return UsersService;
+};
diff --git a/modules/backend/templates/base.html b/modules/backend/templates/base.html
new file mode 100644
index 0000000..4b741b1
--- /dev/null
+++ b/modules/backend/templates/base.html
@@ -0,0 +1,21 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+Hello ${firstName} ${lastName}!<br><br>
+${message}<br><br>
+--------------<br>
+${sign}<br>
\ No newline at end of file
diff --git a/modules/backend/test/app/db.js b/modules/backend/test/app/db.js
new file mode 100644
index 0000000..b1441bf
--- /dev/null
+++ b/modules/backend/test/app/db.js
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+const _ = require('lodash');
+const mongoose = require('mongoose');
+
+const testAccounts = require('../data/accounts.json');
+const testClusters = require('../data/clusters.json');
+const testCaches = require('../data/caches.json');
+const testDomains = require('../data/domains.json');
+const testIgfss = require('../data/igfss.json');
+const testSpaces = require('../data/spaces.json');
+
+module.exports = {
+    implements: 'dbHelper',
+    inject: ['mongo']
+};
+
+module.exports.factory = (mongo) => {
+    const prepareUserSpaces = () => Promise.all([mongo.Account.create(testAccounts), mongo.Space.create(testSpaces)]);
+    const prepareClusters = () => mongo.Cluster.create(testClusters);
+    const prepareDomains = () => mongo.DomainModel.create(testDomains);
+    const prepareCaches = () => mongo.Cache.create(testCaches);
+    const prepareIgfss = () => mongo.Igfs.create(testIgfss);
+
+    const drop = () => {
+        return Promise.all(_.map(mongoose.connection.collections, (collection) => collection.deleteMany()));
+    };
+
+    const init = () => {
+        return drop()
+            .then(prepareUserSpaces)
+            .then(prepareClusters)
+            .then(prepareDomains)
+            .then(prepareCaches)
+            .then(prepareIgfss);
+    };
+
+    return {
+        drop,
+        init,
+        mocks: {
+            accounts: testAccounts,
+            clusters: testClusters,
+            caches: testCaches,
+            domains: testDomains,
+            igfss: testIgfss,
+            spaces: testSpaces
+        }
+    };
+};
diff --git a/modules/backend/test/app/httpAgent.js b/modules/backend/test/app/httpAgent.js
new file mode 100644
index 0000000..2b660fa
--- /dev/null
+++ b/modules/backend/test/app/httpAgent.js
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'agentFactory',
+    inject: ['api-server', 'require(http)', 'require(supertest)']
+};
+
+module.exports.factory = (apiSrv, http, request) => {
+    const express = apiSrv.attach(http.createServer());
+    let authAgentInstance = null;
+
+    return {
+        authAgent: ({email, password}) => {
+            if (authAgentInstance)
+                return Promise.resolve(authAgentInstance);
+
+            return new Promise((resolve, reject) => {
+                authAgentInstance = request.agent(express);
+                authAgentInstance.post('/api/v1/signin')
+                    .send({email, password})
+                    .end((err, res) => {
+                        if (res.status === 401 || err)
+                            return reject(err);
+
+                        resolve(authAgentInstance);
+                    });
+            });
+        },
+        guestAgent: () => Promise.resolve(request.agent(express))
+    };
+};
diff --git a/modules/backend/test/app/mail.js b/modules/backend/test/app/mail.js
new file mode 100644
index 0000000..2a48123
--- /dev/null
+++ b/modules/backend/test/app/mail.js
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/mails:mock',
+    inject: ['services/mails']
+};
+
+module.exports.factory = (mails) => {
+    mails.send = () => Promise.resolve(true);
+
+    return mails;
+};
diff --git a/modules/backend/test/app/settings.js b/modules/backend/test/app/settings.js
new file mode 100644
index 0000000..58d4706
--- /dev/null
+++ b/modules/backend/test/app/settings.js
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const MongoMemoryServer = require('mongodb-memory-server').default;
+
+// Fire me up!
+
+module.exports = {
+    implements: 'settings:mock',
+    inject: ['settings']
+};
+
+module.exports.factory = (settings) => {
+    const mongoServer = new MongoMemoryServer();
+
+    return mongoServer.getConnectionString()
+        .then((mongoUrl) => {
+            settings.mongoUrl = mongoUrl;
+
+            return settings;
+        });
+};
diff --git a/modules/backend/test/config/settings.json b/modules/backend/test/config/settings.json
new file mode 100644
index 0000000..5796260
--- /dev/null
+++ b/modules/backend/test/config/settings.json
@@ -0,0 +1,17 @@
+{
+  "server": {
+    "port": 3000,
+    "ssl": false
+  },
+  "agentServer": {
+    "port": 3001,
+    "ssl": false
+  },
+  "mail": {
+    "service": "",
+    "sign": "Kind regards,<br>Apache Ignite Team",
+    "from": "Apache Ignite Web Console <someusername@somecompany.tld>",
+    "user": "someusername@somecompany.tld",
+    "pass": ""
+  }
+}
diff --git a/modules/backend/test/data/accounts.json b/modules/backend/test/data/accounts.json
new file mode 100644
index 0000000..9dcb0ed
--- /dev/null
+++ b/modules/backend/test/data/accounts.json
@@ -0,0 +1,19 @@
+[
+  {
+    "_id" : "000000000000000000000001",
+    "salt" : "ca8b49c2eacd498a0973de30c0873c166ed99fa0605981726aedcc85bee17832",
+    "hash" : "c052c87e454cd0875332719e1ce085ccd92bedb73c8f939ba45d387f724da97128280643ad4f841d929d48de802f48f4a27b909d2dc806d957d38a1a4049468ce817490038f00ac1416aaf9f8f5a5c476730b46ea22d678421cd269869d4ba9d194f73906e5d5a4fec5229459e20ebda997fb95298067126f6c15346d886d44b67def03bf3ffe484b2e4fa449985de33a0c12e4e1da4c7d71fe7af5d138433f703d8c7eeebbb3d57f1a89659010a1f1d3cd4fbc524abab07860daabb08f08a28b8bfc64ecde2ea3c103030d0d54fc24d9c02f92ee6b3aa1bcd5c70113ab9a8045faea7dd2dc59ec4f9f69fcf634232721e9fb44012f0e8c8fdf7c6bf642db6867ef8e7877123e1bc78af7604fee2e34ad0191f8b97613ea458e0fca024226b7055e08a4bdb256fabf0a203a1e5b6a6c298fb0c60308569cefba779ce1e41fb971e5d1745959caf524ab0bedafce67157922f9c505cea033f6ed28204791470d9d08d31ce7e8003df8a3a05282d4d60bfe6e2f7de06f4b18377dac0fe764ed683c9b2553e75f8280c748aa166fef6f89190b1c6d369ab86422032171e6f9686de42ac65708e63bf018a043601d85bc5c820c7ad1d51ded32e59cdaa629a3f7ae325bbc931f9f21d90c9204effdbd53721a60c8b180dd8c236133e287a47ccc9e5072eb6593771e435e4d5196d50d6ddb32c226651c6503387895c5ad025f69fd3",
+    "password": "a",
+    "email" : "a@a",
+    "firstName" : "TestFirstName",
+    "lastName" : "TestLastName",
+    "company" : "TestCompany",
+    "country" : "Canada",
+    "admin" : true,
+    "token" : "ppw4tPI3JUOGHva8CODO",
+    "attempts" : 0,
+    "lastLogin" : "2016-06-28T10:41:07.463Z",
+    "__v" : 0,
+    "resetPasswordToken" : "892rnLbEnVp1FP75Jgpi"
+  }
+]
diff --git a/modules/backend/test/data/caches.json b/modules/backend/test/data/caches.json
new file mode 100644
index 0000000..dd14b4e
--- /dev/null
+++ b/modules/backend/test/data/caches.json
@@ -0,0 +1,135 @@
+[
+  {
+    "_id" : "000000000000000000000001",
+    "space": "000000000000000000000001",
+    "name": "CarCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": ["000000000000000000000001", "000000000000000000000002", "000000000000000000000003", "000000000000000000000004", "000000000000000000000005"],
+    "clusters": ["000000000000000000000001"]
+  },
+  {
+    "_id" : "000000000000000000000002",
+    "space": "000000000000000000000001",
+    "name": "ParkingCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": ["000000000000000000000001", "000000000000000000000002", "000000000000000000000003", "000000000000000000000004", "000000000000000000000005"],
+    "clusters": ["000000000000000000000001"]
+  },
+  {
+    "_id" : "000000000000000000000021",
+    "space": "000000000000000000000001",
+    "name": "CarCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": ["000000000000000000000023", "000000000000000000000024", "000000000000000000000025"],
+    "clusters": ["000000000000000000000020"]
+  },
+  {
+    "_id" : "000000000000000000000022",
+    "space": "000000000000000000000001",
+    "name": "ParkingCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": ["000000000000000000000023", "000000000000000000000024", "000000000000000000000025"],
+    "clusters": ["000000000000000000000020"]
+  },
+  {
+    "_id" : "000000000000000000000023",
+    "space": "000000000000000000000001",
+    "name": "CountryCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": ["000000000000000000000023", "000000000000000000000024", "000000000000000000000025"],
+    "clusters": ["000000000000000000000020"]
+  },
+  {
+    "_id" : "000000000000000000000024",
+    "space": "000000000000000000000001",
+    "name": "DepartmentCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": ["000000000000000000000023", "000000000000000000000024", "000000000000000000000025"],
+    "clusters": ["000000000000000000000020"]
+  },
+  {
+    "_id" : "000000000000000000000025",
+    "space": "000000000000000000000001",
+    "name": "EmployeeCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": ["000000000000000000000023", "000000000000000000000024", "000000000000000000000025"],
+    "clusters": ["000000000000000000000020"]
+  }
+]
diff --git a/modules/backend/test/data/clusters.json b/modules/backend/test/data/clusters.json
new file mode 100644
index 0000000..55e05ce
--- /dev/null
+++ b/modules/backend/test/data/clusters.json
@@ -0,0 +1,56 @@
+[
+  {
+    "_id" : "000000000000000000000001",
+    "space": "000000000000000000000001",
+    "name": "cluster-igfs",
+    "connector": {
+      "noDelay": true
+    },
+    "communication": {
+      "tcpNoDelay": true
+    },
+    "igfss": ["000000000000000000000001"],
+    "caches": ["000000000000000000000001", "000000000000000000000002"],
+    "models": ["000000000000000000000001", "000000000000000000000002"],
+    "binaryConfiguration": {
+      "compactFooter": true,
+      "typeConfigurations": []
+    },
+    "discovery": {
+      "kind": "Multicast",
+      "Multicast": {
+        "addresses": ["127.0.0.1:47500..47510"]
+      },
+      "Vm": {
+        "addresses": ["127.0.0.1:47500..47510"]
+      }
+    }
+  },
+  {
+    "_id" : "000000000000000000000020",
+    "space": "000000000000000000000001",
+    "name": "cluster-caches",
+    "connector": {
+      "noDelay": true
+    },
+    "communication": {
+      "tcpNoDelay": true
+    },
+    "igfss": [],
+    "caches": ["000000000000000000000021", "000000000000000000000022", "000000000000000000000023", "000000000000000000000024", "000000000000000000000025"],
+    "models": ["000000000000000000000023", "000000000000000000000024", "000000000000000000000025"],
+    "binaryConfiguration": {
+      "compactFooter": true,
+      "typeConfigurations": []
+    },
+    "discovery": {
+      "kind": "Multicast",
+      "Multicast": {
+        "addresses": ["127.0.0.1:47500..47510"]
+      },
+      "Vm": {
+        "addresses": ["127.0.0.1:47500..47510"]
+      }
+    }
+  }
+]
diff --git a/modules/backend/test/data/domains.json b/modules/backend/test/data/domains.json
new file mode 100644
index 0000000..1585e98
--- /dev/null
+++ b/modules/backend/test/data/domains.json
@@ -0,0 +1,322 @@
+[
+  {
+    "_id" : "000000000000000000000001",
+    "space": "000000000000000000000001",
+    "keyType": "Integer",
+    "valueType": "model.Parking",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "CARS",
+    "databaseTable": "PARKING",
+    "indexes": [],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "name",
+        "className": "String"
+      },
+      {
+        "name": "capacity",
+        "className": "Integer"
+      }
+    ],
+    "valueFields": [
+      {
+        "databaseFieldName": "NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "name",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "CAPACITY",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "capacity",
+        "javaFieldType": "int"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": [],
+    "clusters": ["000000000000000000000001"]
+  },
+  {
+    "_id" : "000000000000000000000002",
+    "space": "000000000000000000000001",
+    "keyType": "Integer",
+    "valueType": "model.Department",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "PUBLIC",
+    "databaseTable": "DEPARTMENT",
+    "indexes": [],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "countryId",
+        "className": "Integer"
+      },
+      {
+        "name": "name",
+        "className": "String"
+      }
+    ],
+    "valueFields": [
+      {
+        "databaseFieldName": "COUNTRY_ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "countryId",
+        "javaFieldType": "int"
+      },
+      {
+        "databaseFieldName": "NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "name",
+        "javaFieldType": "String"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": [],
+    "clusters": ["000000000000000000000001"]
+  },
+  {
+    "_id" : "000000000000000000000023",
+    "space": "000000000000000000000001",
+    "keyType": "Integer",
+    "valueType": "model.Employee",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "PUBLIC",
+    "databaseTable": "EMPLOYEE",
+    "indexes": [
+      {
+        "name": "EMP_NAMES",
+        "indexType": "SORTED",
+        "fields": [
+          {
+            "name": "firstName",
+            "direction": true
+          },
+          {
+            "name": "lastName",
+            "direction": true
+          }
+        ]
+      },
+      {
+        "name": "EMP_SALARY",
+        "indexType": "SORTED",
+        "fields": [
+          {
+            "name": "salary",
+            "direction": true
+          }
+        ]
+      }
+    ],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "departmentId",
+        "className": "Integer"
+      },
+      {
+        "name": "managerId",
+        "className": "Integer"
+      },
+      {
+        "name": "firstName",
+        "className": "String"
+      },
+      {
+        "name": "lastName",
+        "className": "String"
+      },
+      {
+        "name": "email",
+        "className": "String"
+      },
+      {
+        "name": "phoneNumber",
+        "className": "String"
+      },
+      {
+        "name": "hireDate",
+        "className": "Date"
+      },
+      {
+        "name": "job",
+        "className": "String"
+      },
+      {
+        "name": "salary",
+        "className": "Double"
+      }
+    ],
+    "valueFields": [
+      {
+        "databaseFieldName": "DEPARTMENT_ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "departmentId",
+        "javaFieldType": "int"
+      },
+      {
+        "databaseFieldName": "MANAGER_ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "managerId",
+        "javaFieldType": "Integer"
+      },
+      {
+        "databaseFieldName": "FIRST_NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "firstName",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "LAST_NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "lastName",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "EMAIL",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "email",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "PHONE_NUMBER",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "phoneNumber",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "HIRE_DATE",
+        "databaseFieldType": "DATE",
+        "javaFieldName": "hireDate",
+        "javaFieldType": "Date"
+      },
+      {
+        "databaseFieldName": "JOB",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "job",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "SALARY",
+        "databaseFieldType": "DOUBLE",
+        "javaFieldName": "salary",
+        "javaFieldType": "Double"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": [],
+    "clusters": ["000000000000000000000002"]
+  },
+  {
+    "_id" : "000000000000000000000024",
+    "space": "000000000000000000000001",
+    "keyType": "Integer",
+    "valueType": "model.Country",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "PUBLIC",
+    "databaseTable": "COUNTRY",
+    "indexes": [],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "name",
+        "className": "String"
+      },
+      {
+        "name": "population",
+        "className": "Integer"
+      }
+    ],
+    "valueFields": [
+      {
+        "databaseFieldName": "NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "name",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "POPULATION",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "population",
+        "javaFieldType": "int"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": [],
+    "clusters": ["000000000000000000000002"]
+  },
+  {
+    "_id" : "000000000000000000000025",
+    "space": "000000000000000000000001",
+    "keyType": "Integer",
+    "valueType": "model.Car",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "CARS",
+    "databaseTable": "CAR",
+    "indexes": [],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "parkingId",
+        "className": "Integer"
+      },
+      {
+        "name": "name",
+        "className": "String"
+      }
+    ],
+    "valueFields": [
+      {
+        "databaseFieldName": "PARKING_ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "parkingId",
+        "javaFieldType": "int"
+      },
+      {
+        "databaseFieldName": "NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "name",
+        "javaFieldType": "String"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": [],
+    "clusters": ["000000000000000000000002"]
+  }
+]
diff --git a/modules/backend/test/data/igfss.json b/modules/backend/test/data/igfss.json
new file mode 100644
index 0000000..c1f0645
--- /dev/null
+++ b/modules/backend/test/data/igfss.json
@@ -0,0 +1,12 @@
+[
+  {
+    "_id" : "000000000000000000000001",
+    "space": "000000000000000000000001",
+    "ipcEndpointEnabled": true,
+    "fragmentizerEnabled": true,
+    "name": "igfs",
+    "dataCacheName": "igfs-data",
+    "metaCacheName": "igfs-meta",
+    "clusters": ["000000000000000000000001"]
+  }
+]
diff --git a/modules/backend/test/data/spaces.json b/modules/backend/test/data/spaces.json
new file mode 100644
index 0000000..519f7fe
--- /dev/null
+++ b/modules/backend/test/data/spaces.json
@@ -0,0 +1,14 @@
+[
+  {
+    "_id": "000000000000000000000001",
+    "name": "Personal space",
+    "owner": "000000000000000000000001",
+    "demo": false
+  },
+  {
+    "_id": "000000000000000000000002",
+    "name": "Demo space",
+    "owner": "000000000000000000000001",
+    "demo": true
+  }
+]
diff --git a/modules/backend/test/index.js b/modules/backend/test/index.js
new file mode 100644
index 0000000..258a876
--- /dev/null
+++ b/modules/backend/test/index.js
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const Mocha = require('mocha');
+const glob = require('glob');
+
+const mocha = new Mocha({ui: 'tdd', reporter: process.env.MOCHA_REPORTER || 'spec'});
+const testPath = ['./test/unit/**/*.js', './test/routes/**/*.js'];
+
+testPath
+    .map((mask) => glob.sync(mask))
+    .reduce((acc, items) => acc.concat(items), [])
+    .map(mocha.addFile.bind(mocha));
+
+const runner = mocha.run();
+
+runner.on('end', (failures) => process.exit(failures));
diff --git a/modules/backend/test/injector.js b/modules/backend/test/injector.js
new file mode 100644
index 0000000..25f7b66
--- /dev/null
+++ b/modules/backend/test/injector.js
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const path = require('path');
+const fireUp = require('fire-up');
+
+module.exports = fireUp.newInjector({
+    basePath: path.join(__dirname, '../'),
+    modules: [
+        './app/**/*.js',
+        './config/**/*.js',
+        './errors/**/*.js',
+        './middlewares/**/*.js',
+        './routes/**/*.js',
+        './services/**/*.js',
+        './test/app/*.js'
+    ],
+    use: [
+        'settings:mock',
+        'services/mails:mock'
+    ]
+});
diff --git a/modules/backend/test/routes/clusters.js b/modules/backend/test/routes/clusters.js
new file mode 100644
index 0000000..c682ec5
--- /dev/null
+++ b/modules/backend/test/routes/clusters.js
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const assert = require('chai').assert;
+const injector = require('../injector');
+
+let agentFactory;
+let db;
+
+suite('routes.clusters', () => {
+    suiteSetup(() => {
+        return Promise.all([injector('agentFactory'), injector('dbHelper')])
+            .then(([_agent, _db]) => {
+                agentFactory = _agent;
+                db = _db;
+            });
+    });
+
+    setup(() => {
+        return db.init();
+    });
+
+    test('Save cluster model', (done) => {
+        const cluster = Object.assign({}, db.mocks.clusters[0], {name: 'newClusterName'});
+
+        agentFactory.authAgent(db.mocks.accounts[0])
+            .then((agent) => {
+                agent.put('/api/v1/configuration/clusters')
+                    .send({cluster})
+                    .expect(200)
+                    .expect((res) => {
+                        assert.isNotNull(res.body);
+                        assert.equal(res.body.rowsAffected, 1);
+                    })
+                    .end(done);
+            })
+            .catch(done);
+    });
+
+    test('Remove cluster model', (done) => {
+        agentFactory.authAgent(db.mocks.accounts[0])
+            .then((agent) => {
+                agent.post('/api/v1/configuration/clusters/remove')
+                    .send({_id: db.mocks.clusters[0]._id})
+                    .expect(200)
+                    .expect((res) => {
+                        assert.isNotNull(res.body);
+                        assert.equal(res.body.rowsAffected, 1);
+                    })
+                    .end(done);
+            })
+            .catch(done);
+    });
+
+    test('Remove all clusters', (done) => {
+        agentFactory.authAgent(db.mocks.accounts[0])
+            .then((agent) => {
+                agent.post('/api/v1/configuration/clusters/remove/all')
+                    .expect(200)
+                    .expect((res) => {
+                        assert.isNotNull(res.body);
+                        assert.equal(res.body.rowsAffected, db.mocks.clusters.length);
+                    })
+                    .end(done);
+            })
+            .catch(done);
+    });
+});
diff --git a/modules/backend/test/routes/public.js b/modules/backend/test/routes/public.js
new file mode 100644
index 0000000..fe7793c
--- /dev/null
+++ b/modules/backend/test/routes/public.js
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const assert = require('chai').assert;
+const injector = require('../injector');
+
+const testAccounts = require('../data/accounts.json');
+
+let agentFactory;
+let db;
+
+suite('routes.public', () => {
+    suiteSetup(() => {
+        return Promise.all([injector('agentFactory'), injector('dbHelper')])
+            .then(([_agent, _db]) => {
+                agentFactory = _agent;
+                db = _db;
+            });
+    });
+
+    setup(() => {
+        return db.init();
+    });
+
+    test('Login success', (done) => {
+        const user = testAccounts[0];
+
+        agentFactory.guestAgent()
+            .then((agent) => {
+                agent.post('/api/v1/signin')
+                    .send({email: user.email, password: user.password})
+                    .expect(200)
+                    .expect((res) => {
+                        assert.isNotNull(res.headers['set-cookie']);
+                        assert.match(res.headers['set-cookie'], /connect\.sid/);
+                    })
+                    .end(done);
+            })
+            .catch(done);
+    });
+
+    test('Login fail', (done) => {
+        const user = testAccounts[0];
+
+        agentFactory.guestAgent()
+            .then((agent) => {
+                agent.post('/api/v1/signin')
+                    .send({email: user.email, password: 'notvalidpassword'})
+                    .expect(401)
+                    .end(done);
+            })
+            .catch(done);
+    });
+});
diff --git a/modules/backend/test/unit/ActivitiesService.test.js b/modules/backend/test/unit/ActivitiesService.test.js
new file mode 100644
index 0000000..628c4d8
--- /dev/null
+++ b/modules/backend/test/unit/ActivitiesService.test.js
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const assert = require('chai').assert;
+const injector = require('../injector');
+const testAccounts = require('../data/accounts.json');
+
+let activitiesService;
+let mongo;
+let db;
+
+const testAccount = testAccounts[0];
+const owner = testAccount._id;
+const group = 'test';
+const action1 = '/test/activity1';
+const action2 = '/test/activity2';
+
+suite('ActivitiesServiceTestsSuite', () => {
+    suiteSetup(() => {
+        return Promise.all([
+            injector('services/activities'),
+            injector('mongo'),
+            injector('dbHelper')
+        ])
+            .then(([_activitiesService, _mongo, _db]) => {
+                mongo = _mongo;
+                activitiesService = _activitiesService;
+                db = _db;
+            });
+    });
+
+    setup(() => db.init());
+
+    test('Activities creation and update', (done) => {
+        activitiesService.merge(testAccount, { group, action: action1 })
+            .then((activity) => {
+                assert.isNotNull(activity);
+                assert.equal(activity.amount, 1);
+
+                return mongo.Activities.findById(activity._id);
+            })
+            .then((activityDoc) => {
+                assert.isNotNull(activityDoc);
+                assert.equal(activityDoc.amount, 1);
+            })
+            .then(() => activitiesService.merge(testAccount, { group, action: action1 }))
+            .then((activity) => {
+                assert.isNotNull(activity);
+                assert.equal(activity.amount, 2);
+
+                return mongo.Activities.findById(activity._id);
+            })
+            .then((activityDoc) => {
+                assert.isNotNull(activityDoc);
+                assert.equal(activityDoc.amount, 2);
+            })
+            .then(done)
+            .catch(done);
+    });
+
+    test('Activities total and detail information', (done) => {
+        const startDate = new Date();
+
+        startDate.setDate(1);
+        startDate.setHours(0, 0, 0, 0);
+
+        const endDate = new Date(startDate);
+        endDate.setMonth(endDate.getMonth() + 1);
+
+        Promise.all([
+            activitiesService.merge(testAccount, {group, action: action1}),
+            activitiesService.merge(testAccount, {group, action: action2})
+        ])
+            .then(() => activitiesService.total(owner, {startDate, endDate}))
+            .then((activities) =>
+                assert.equal(activities[owner].test, 2)
+            )
+            .then(() => activitiesService.detail(owner, {startDate, endDate}))
+            .then((activities) =>
+                assert.deepEqual(activities[owner], {
+                    '/test/activity2': 1, '/test/activity1': 1
+                })
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('Activities periods', (done) => {
+        const startDate = new Date();
+
+        startDate.setDate(1);
+        startDate.setHours(0, 0, 0, 0);
+
+        const nextMonth = (baseDate) => {
+            const date = new Date(baseDate);
+
+            date.setMonth(date.getMonth() + 1);
+
+            return date;
+        };
+
+        const borderDate = nextMonth(startDate);
+        const endDate = nextMonth(borderDate);
+
+        activitiesService.merge(testAccount, { group, action: action1 })
+            .then(() => activitiesService.merge(testAccount, { group, action: action1 }, borderDate))
+            .then(() => activitiesService.total({ startDate, endDate: borderDate }))
+            .then((activities) =>
+                assert.equal(activities[owner].test, 1)
+            )
+            .then(() => activitiesService.total({ startDate: borderDate, endDate }))
+            .then((activities) =>
+                assert.equal(activities[owner].test, 1)
+            )
+            .then(done)
+            .catch(done);
+    });
+});
diff --git a/modules/backend/test/unit/AuthService.test.js b/modules/backend/test/unit/AuthService.test.js
new file mode 100644
index 0000000..d9cbec7
--- /dev/null
+++ b/modules/backend/test/unit/AuthService.test.js
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const assert = require('chai').assert;
+const injector = require('../injector');
+const testAccounts = require('../data/accounts.json');
+
+let authService;
+let errors;
+let db;
+
+suite('AuthServiceTestsSuite', () => {
+    suiteSetup(() => {
+        return Promise.all([injector('services/auth'),
+            injector('errors'),
+            injector('dbHelper')])
+            .then(([_authService, _errors, _db]) => {
+                authService = _authService;
+                errors = _errors;
+                db = _db;
+            });
+    });
+
+    setup(() => db.init());
+
+    test('Reset password token for non existing user', (done) => {
+        authService.resetPasswordToken('non-exisitng@email.ee')
+            .catch((err) => {
+                assert.instanceOf(err, errors.MissingResourceException);
+                done();
+            });
+    });
+
+    test('Reset password token for existing user', (done) => {
+        authService.resetPasswordToken(null, testAccounts[0].email)
+            .then((account) => {
+                assert.notEqual(account.resetPasswordToken.length, 0);
+                assert.notEqual(account.resetPasswordToken, testAccounts[0].resetPasswordToken);
+            })
+            .then(done)
+            .catch(done);
+    });
+
+    test('Reset password by token for non existing user', (done) => {
+        authService.resetPasswordByToken('0')
+            .catch((err) => {
+                assert.instanceOf(err, errors.MissingResourceException);
+                done();
+            });
+    });
+
+    test('Reset password by token for existing user', (done) => {
+        authService.resetPasswordByToken(null, testAccounts[0].resetPasswordToken, 'NewUniquePassword$1')
+            .then((account) => {
+                assert.isUndefined(account.resetPasswordToken);
+                assert.notEqual(account.hash, 0);
+                assert.notEqual(account.hash, testAccounts[0].hash);
+            })
+            .then(done)
+            .catch(done);
+    });
+
+    test('Validate user for non existing reset token', (done) => {
+        authService.validateResetToken('Non existing token')
+            .catch((err) => {
+                assert.instanceOf(err, errors.IllegalAccessError);
+                done();
+            });
+    });
+
+    test('Validate reset token', (done) => {
+        authService.validateResetToken(testAccounts[0].resetPasswordToken)
+            .then(({token, email}) => {
+                assert.equal(email, testAccounts[0].email);
+                assert.equal(token, testAccounts[0].resetPasswordToken);
+            })
+            .then(done)
+            .catch(done);
+    });
+});
diff --git a/modules/backend/test/unit/CacheService.test.js b/modules/backend/test/unit/CacheService.test.js
new file mode 100644
index 0000000..419b9f7
--- /dev/null
+++ b/modules/backend/test/unit/CacheService.test.js
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const assert = require('chai').assert;
+const injector = require('../injector');
+const testCaches = require('../data/caches.json');
+const testAccounts = require('../data/accounts.json');
+const testSpaces = require('../data/spaces.json');
+
+let cachesService;
+let mongo;
+let errors;
+let db;
+
+suite('CacheServiceTestsSuite', () => {
+    suiteSetup(() => {
+        return Promise.all([injector('services/caches'),
+            injector('mongo'),
+            injector('errors'),
+            injector('dbHelper')])
+            .then(([_cacheService, _mongo, _errors, _db]) => {
+                mongo = _mongo;
+                cachesService = _cacheService;
+                errors = _errors;
+                db = _db;
+            });
+    });
+
+    setup(() => db.init());
+
+    test('Get cache', (done) => {
+        const _id = testCaches[0]._id;
+
+        cachesService.get(testCaches[0].space, false, _id)
+            .then((cache) => {
+                assert.isNotNull(cache);
+                assert.equal(cache._id, _id);
+            })
+            .then(done)
+            .catch(done);
+    });
+
+    test('Create new cache', (done) => {
+        const dupleCache = Object.assign({}, testCaches[0], {name: 'Other name'});
+
+        delete dupleCache._id;
+
+        cachesService.merge(dupleCache)
+            .then((cache) => mongo.Cache.findById(cache._id))
+            .then((cache) => assert.isNotNull(cache))
+            .then(done)
+            .catch(done);
+    });
+
+    test('Update existed cache', (done) => {
+        const newName = 'NewUniqueName';
+
+        const cacheBeforeMerge = Object.assign({}, testCaches[0], {name: newName});
+
+        cachesService.merge(cacheBeforeMerge)
+            .then((cache) => mongo.Cache.findById(cache._id))
+            .then((cacheAfterMerge) => assert.equal(cacheAfterMerge.name, newName))
+            .then(done)
+            .catch(done);
+    });
+
+    test('Create duplicated cache', (done) => {
+        const dupleCache = Object.assign({}, testCaches[0]);
+
+        delete dupleCache._id;
+
+        cachesService.merge(dupleCache)
+            .catch((err) => {
+                assert.instanceOf(err, errors.DuplicateKeyException);
+
+                done();
+            });
+    });
+
+    test('Remove existed cache', (done) => {
+        cachesService.remove(testCaches[0]._id)
+            .then(({rowsAffected}) =>
+                assert.equal(rowsAffected, 1)
+            )
+            .then(() => mongo.Cache.findById(testCaches[0]._id))
+            .then((notFoundCache) =>
+                assert.isNull(notFoundCache)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('Remove cache without identifier', (done) => {
+        cachesService.remove()
+            .catch((err) => {
+                assert.instanceOf(err, errors.IllegalArgumentException);
+
+                done();
+            });
+    });
+
+    test('Remove missed cache', (done) => {
+        const validNoExistingId = 'FFFFFFFFFFFFFFFFFFFFFFFF';
+
+        cachesService.remove(validNoExistingId)
+            .then(({rowsAffected}) =>
+                assert.equal(rowsAffected, 0)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('Get all caches by space', (done) => {
+        cachesService.listBySpaces(testSpaces[0]._id)
+            .then((caches) =>
+                assert.equal(caches.length, 7)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('Remove all caches in space', (done) => {
+        cachesService.removeAll(testAccounts[0]._id, false)
+            .then(({rowsAffected}) =>
+                assert.equal(rowsAffected, 7)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('List of all caches in cluster', (done) => {
+        cachesService.shortList(testAccounts[0]._id, false, testCaches[0].clusters[0])
+            .then((caches) => {
+                assert.equal(caches.length, 2);
+                assert.isNotNull(caches[0]._id);
+                assert.isNotNull(caches[0].name);
+                assert.isNotNull(caches[0].cacheMode);
+                assert.isNotNull(caches[0].atomicityMode);
+            })
+            .then(done)
+            .catch(done);
+    });
+
+    test('Update linked entities on update cache', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+
+    test('Update linked entities on remove cache', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+
+    test('Update linked entities on remove all caches in space', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+});
diff --git a/modules/backend/test/unit/ClusterService.test.js b/modules/backend/test/unit/ClusterService.test.js
new file mode 100644
index 0000000..93edfc1
--- /dev/null
+++ b/modules/backend/test/unit/ClusterService.test.js
@@ -0,0 +1,376 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const _ = require('lodash');
+const assert = require('chai').assert;
+const injector = require('../injector');
+
+const testClusters = require('../data/clusters.json');
+const testCaches = require('../data/caches.json');
+const testAccounts = require('../data/accounts.json');
+const testSpaces = require('../data/spaces.json');
+
+let clusterService;
+let cacheService;
+let mongo;
+let errors;
+let db;
+
+suite('ClusterServiceTestsSuite', () => {
+    suiteSetup(() => {
+        return Promise.all([injector('services/clusters'),
+            injector('services/caches'),
+            injector('mongo'),
+            injector('errors'),
+            injector('dbHelper')])
+            .then(([_clusterService, _cacheService, _mongo, _errors, _db]) => {
+                mongo = _mongo;
+                clusterService = _clusterService;
+                cacheService = _cacheService;
+                errors = _errors;
+                db = _db;
+            });
+    });
+
+    setup(() => db.init());
+
+    test('Get cluster', (done) => {
+        const _id = testClusters[0]._id;
+
+        clusterService.get(testClusters[0].space, false, _id)
+            .then((cluster) => {
+                assert.isNotNull(cluster);
+                assert.equal(cluster._id, _id);
+            })
+            .then(done)
+            .catch(done);
+    });
+
+    test('Create new cluster', (done) => {
+        const dupleCluster = Object.assign({}, testClusters[0], {name: 'Other name'});
+
+        delete dupleCluster._id;
+
+        clusterService.merge(dupleCluster)
+            .then((cluster) => mongo.Cluster.findById(cluster._id))
+            .then((cluster) => assert.isNotNull(cluster))
+            .then(done)
+            .catch(done);
+    });
+
+    test('Update existed cluster', (done) => {
+        const newName = 'NewUniqueName';
+
+        const clusterBeforeMerge = Object.assign({}, testClusters[0], {name: newName});
+
+        clusterService.merge(clusterBeforeMerge)
+            .then((cluster) => mongo.Cluster.findById(cluster._id))
+            .then((clusterAfterMerge) => assert.equal(clusterAfterMerge.name, newName))
+            .then(done)
+            .catch(done);
+    });
+
+    test('Create duplicated cluster', (done) => {
+        const dupleCluster = Object.assign({}, testClusters[0]);
+
+        delete dupleCluster._id;
+
+        clusterService.merge(dupleCluster)
+            .catch((err) => {
+                assert.instanceOf(err, errors.DuplicateKeyException);
+
+                done();
+            });
+    });
+
+    test('Remove existed cluster', (done) => {
+        clusterService.remove(testClusters[0]._id)
+            .then(({rowsAffected}) =>
+                assert.equal(rowsAffected, 1)
+            )
+            .then(() => mongo.Cluster.findById(testClusters[0]._id))
+            .then((notFoundCluster) =>
+                assert.isNull(notFoundCluster)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('Remove cluster without identifier', (done) => {
+        clusterService.remove()
+            .catch((err) => {
+                assert.instanceOf(err, errors.IllegalArgumentException);
+
+                done();
+            });
+    });
+
+    test('Remove missed cluster', (done) => {
+        const validNoExistingId = 'FFFFFFFFFFFFFFFFFFFFFFFF';
+
+        clusterService.remove(validNoExistingId)
+            .then(({rowsAffected}) =>
+                assert.equal(rowsAffected, 0)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('Get all clusters by space', (done) => {
+        clusterService.listBySpaces(testSpaces[0]._id)
+            .then((clusters) =>
+                assert.equal(clusters.length, 2)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('Remove all clusters in space', (done) => {
+        clusterService.removeAll(testAccounts[0]._id, false)
+            .then(({rowsAffected}) =>
+                assert.equal(rowsAffected, 2)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('List of all clusters in space', (done) => {
+        clusterService.shortList(testAccounts[0]._id, false)
+            .then((clusters) => {
+                assert.equal(clusters.length, 2);
+
+                assert.equal(clusters[0].name, 'cluster-igfs');
+                assert.isNotNull(clusters[0].discovery);
+                assert.equal(clusters[0].cachesCount, 2);
+                assert.equal(clusters[0].modelsCount, 2);
+                assert.equal(clusters[0].igfsCount, 1);
+
+                assert.equal(clusters[1].name, 'cluster-caches');
+                assert.isNotNull(clusters[1].discovery);
+                assert.equal(clusters[1].cachesCount, 5);
+                assert.equal(clusters[1].modelsCount, 3);
+                assert.equal(clusters[1].igfsCount, 0);
+            })
+            .then(done)
+            .catch(done);
+    });
+
+    test('Create new cluster from basic', (done) => {
+        const cluster = _.head(testClusters);
+        const caches = _.filter(testCaches, ({_id}) => _.includes(cluster.caches, _id));
+
+        db.drop()
+            .then(() => Promise.all([mongo.Account.create(testAccounts), mongo.Space.create(testSpaces)]))
+            .then(() => clusterService.upsertBasic(testAccounts[0]._id, false, {cluster, caches}))
+            .then((output) => {
+                assert.isNotNull(output);
+
+                assert.equal(output.rowsAffected, 1);
+            })
+            .then(() => clusterService.get(testAccounts[0]._id, false, cluster._id))
+            .then((savedCluster) => {
+                assert.isNotNull(savedCluster);
+
+                assert.equal(savedCluster._id, cluster._id);
+                assert.equal(savedCluster.name, cluster.name);
+                assert.notStrictEqual(savedCluster.caches, cluster.caches);
+
+                assert.notStrictEqual(savedCluster, cluster);
+            })
+            .then(() => cacheService.get(testAccounts[0]._id, false, caches[0]._id))
+            .then((cb1) => {
+                assert.isNotNull(cb1);
+            })
+            .then(() => cacheService.get(testAccounts[0]._id, false, caches[1]._id))
+            .then((cb2) => {
+                assert.isNotNull(cb2);
+            })
+            .then(done)
+            .catch(done);
+    });
+
+    // test('Create new cluster without space', (done) => {
+    //     const cluster = _.cloneDeep(_.head(testClusters));
+    //     const caches = _.filter(testCaches, ({_id}) => _.includes(cluster.caches, _id));
+    //
+    //     delete cluster.space;
+    //
+    //     db.drop()
+    //         .then(() => Promise.all([mongo.Account.create(testAccounts), mongo.Space.create(testSpaces)]))
+    //         .then(() => clusterService.upsertBasic(testAccounts[0]._id, false, {cluster, caches}))
+    //         .then(() => done())
+    //         .catch(done);
+    // });
+
+    test('Create new cluster with duplicated name', (done) => {
+        const cluster = _.cloneDeep(_.head(testClusters));
+        const caches = _.filter(testCaches, ({_id}) => _.includes(cluster.caches, _id));
+
+        cluster.name = _.last(testClusters).name;
+
+        clusterService.upsertBasic(testAccounts[0]._id, false, {cluster, caches})
+            .then(done)
+            .catch((err) => {
+                assert.instanceOf(err, errors.DuplicateKeyException);
+
+                done();
+            });
+    });
+
+    test('Update cluster from basic', (done) => {
+        const cluster = _.cloneDeep(_.head(testClusters));
+
+        cluster.communication.tcpNoDelay = false;
+        cluster.igfss = [];
+
+        cluster.memoryConfiguration = {
+            defaultMemoryPolicySize: 10,
+            memoryPolicies: [
+                {
+                    name: 'default',
+                    maxSize: 100
+                }
+            ]
+        };
+
+        cluster.caches = _.dropRight(cluster.caches, 1);
+
+        const caches = _.filter(testCaches, ({_id}) => _.includes(cluster.caches, _id));
+
+        _.head(caches).cacheMode = 'REPLICATED';
+        _.head(caches).readThrough = false;
+
+        clusterService.upsertBasic(testAccounts[0]._id, false, {cluster, caches})
+            .then(() => clusterService.get(testAccounts[0]._id, false, cluster._id))
+            .then((savedCluster) => {
+                assert.isNotNull(savedCluster);
+
+                assert.deepEqual(_.invokeMap(savedCluster.caches, 'toString'), cluster.caches);
+
+                _.forEach(savedCluster.memoryConfiguration.memoryPolicies, (plc) => delete plc._id);
+
+                assert.notExists(savedCluster.memoryConfiguration.defaultMemoryPolicySize);
+                assert.deepEqual(savedCluster.memoryConfiguration.memoryPolicies, cluster.memoryConfiguration.memoryPolicies);
+
+                assert.notDeepEqual(_.invokeMap(savedCluster.igfss, 'toString'), cluster.igfss);
+                assert.notDeepEqual(savedCluster.communication, cluster.communication);
+            })
+            .then(() => cacheService.get(testAccounts[0]._id, false, _.head(caches)._id))
+            .then((cb1) => {
+                assert.isNotNull(cb1);
+                assert.equal(cb1.cacheMode, 'REPLICATED');
+                assert.isTrue(cb1.readThrough);
+            })
+            .then(() => cacheService.get(testAccounts[0]._id, false, _.head(testClusters).caches[1]))
+            .then((c2) => {
+                assert.isNull(c2);
+            })
+            .then(done)
+            .catch(done);
+    });
+
+    test('Update cluster from basic with cache removing', (done) => {
+        const cluster = _.cloneDeep(_.head(testClusters));
+
+        const removedCache = _.head(cluster.caches);
+        const upsertedCache = _.last(cluster.caches);
+
+        _.pull(cluster.caches, removedCache);
+
+        const caches = _.filter(testCaches, ({_id}) => _.includes(cluster.caches, _id));
+
+        db.drop()
+            .then(() => Promise.all([mongo.Account.create(testAccounts), mongo.Space.create(testSpaces)]))
+            .then(() => clusterService.upsertBasic(testAccounts[0]._id, false, {cluster, caches}))
+            .then(() => cacheService.get(testAccounts[0]._id, false, removedCache))
+            .then((cache) => {
+                assert.isNull(cache);
+            })
+            .then(() => cacheService.get(testAccounts[0]._id, false, upsertedCache))
+            .then((cache) => {
+                assert.isNotNull(cache);
+
+                done();
+            })
+            .catch(done);
+    });
+
+    test('Update cluster from advanced with cache removing', (done) => {
+        const cluster = _.cloneDeep(_.head(testClusters));
+
+        cluster.communication.tcpNoDelay = false;
+        cluster.igfss = [];
+
+        cluster.memoryConfiguration = {
+            defaultMemoryPolicySize: 10,
+            memoryPolicies: [
+                {
+                    name: 'default',
+                    maxSize: 100
+                }
+            ]
+        };
+
+        const removedCache = _.head(cluster.caches);
+        const upsertedCache = _.last(cluster.caches);
+
+        _.pull(cluster.caches, removedCache);
+
+        const caches = _.filter(testCaches, ({_id}) => _.includes(cluster.caches, _id));
+
+        clusterService.upsert(testAccounts[0]._id, false, {cluster, caches})
+            .then(() => clusterService.get(testAccounts[0]._id, false, cluster._id))
+            .then((savedCluster) => {
+                assert.isNotNull(savedCluster);
+
+                assert.deepEqual(_.invokeMap(savedCluster.caches, 'toString'), cluster.caches);
+
+                _.forEach(savedCluster.memoryConfiguration.memoryPolicies, (plc) => delete plc._id);
+
+                assert.deepEqual(savedCluster.memoryConfiguration, cluster.memoryConfiguration);
+
+                assert.deepEqual(_.invokeMap(savedCluster.igfss, 'toString'), cluster.igfss);
+                assert.deepEqual(savedCluster.communication, cluster.communication);
+            })
+            .then(() => cacheService.get(testAccounts[0]._id, false, removedCache))
+            .then((cache) => {
+                assert.isNull(cache);
+            })
+            .then(() => cacheService.get(testAccounts[0]._id, false, upsertedCache))
+            .then((cache) => {
+                assert.isNotNull(cache);
+
+                done();
+            })
+            .catch(done);
+    });
+
+    test('Update linked entities on update cluster', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+
+    test('Update linked entities on remove cluster', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+
+    test('Update linked entities on remove all clusters in space', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+});
diff --git a/modules/backend/test/unit/DomainService.test.js b/modules/backend/test/unit/DomainService.test.js
new file mode 100644
index 0000000..e4c531d
--- /dev/null
+++ b/modules/backend/test/unit/DomainService.test.js
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const assert = require('chai').assert;
+const injector = require('../injector');
+const testDomains = require('../data/domains.json');
+const testAccounts = require('../data/accounts.json');
+const testSpaces = require('../data/spaces.json');
+
+let domainService;
+let mongo;
+let errors;
+let db;
+
+suite('DomainsServiceTestsSuite', () => {
+    suiteSetup(() => {
+        return Promise.all([injector('services/domains'),
+            injector('mongo'),
+            injector('errors'),
+            injector('dbHelper')])
+            .then(([_domainService, _mongo, _errors, _db]) => {
+                mongo = _mongo;
+                domainService = _domainService;
+                errors = _errors;
+                db = _db;
+            });
+    });
+
+    setup(() => db.init());
+
+    test('Create new domain', (done) => {
+        const dupleDomain = Object.assign({}, testDomains[0], {valueType: 'other.Type'});
+
+        delete dupleDomain._id;
+
+        domainService.batchMerge([dupleDomain])
+            .then((results) => {
+                const domain = results.savedDomains[0];
+
+                assert.isObject(domain);
+                assert.isDefined(domain._id);
+
+                return mongo.DomainModel.findById(domain._id);
+            })
+            .then((domain) => {
+                assert.isObject(domain);
+                assert.isDefined(domain._id);
+            })
+            .then(done)
+            .catch(done);
+    });
+
+    test('Update existed domain', (done) => {
+        const newValType = 'value.Type';
+
+        const domainBeforeMerge = Object.assign({}, testDomains[0], {valueType: newValType});
+
+        domainService.batchMerge([domainBeforeMerge])
+            .then(({savedDomains, generatedCaches}) => {
+                assert.isArray(savedDomains);
+                assert.isArray(generatedCaches);
+
+                assert.equal(1, savedDomains.length);
+                assert.equal(0, generatedCaches.length);
+
+                return mongo.DomainModel.findById(savedDomains[0]._id);
+            })
+            .then((domainAfterMerge) =>
+                assert.equal(domainAfterMerge.valueType, newValType)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('Create duplicated domain', (done) => {
+        const dupleDomain = Object.assign({}, testDomains[0]);
+
+        delete dupleDomain._id;
+
+        domainService.batchMerge([dupleDomain])
+            .catch((err) => {
+                assert.instanceOf(err, errors.DuplicateKeyException);
+
+                done();
+            });
+    });
+
+    test('Remove existed domain', (done) => {
+        domainService.remove(testDomains[0]._id)
+            .then(({rowsAffected}) =>
+                assert.equal(rowsAffected, 1)
+            )
+            .then(() => mongo.DomainModel.findById(testDomains[0]._id))
+            .then((notFoundDomain) =>
+                assert.isNull(notFoundDomain)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('Remove domain without identifier', (done) => {
+        domainService.remove()
+            .catch((err) => {
+                assert.instanceOf(err, errors.IllegalArgumentException);
+
+                done();
+            });
+    });
+
+    test('Remove missed domain', (done) => {
+        const validNoExistingId = 'FFFFFFFFFFFFFFFFFFFFFFFF';
+
+        domainService.remove(validNoExistingId)
+            .then(({rowsAffected}) =>
+                assert.equal(rowsAffected, 0)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('Get all domains by space', (done) => {
+        domainService.listBySpaces(testSpaces[0]._id)
+            .then((domains) =>
+                assert.equal(domains.length, 5)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('Remove all domains in space', (done) => {
+        domainService.removeAll(testAccounts[0]._id, false)
+            .then(({rowsAffected}) =>
+                assert.equal(rowsAffected, 5)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('List of domains in cluster', (done) => {
+        // TODO IGNITE-5737 Add test.
+        done();
+    });
+
+    test('Update linked entities on update domain', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+
+    test('Update linked entities on remove domain', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+
+    test('Update linked entities on remove all domains in space', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+});
diff --git a/modules/backend/test/unit/IgfsService.test.js b/modules/backend/test/unit/IgfsService.test.js
new file mode 100644
index 0000000..67d9e08
--- /dev/null
+++ b/modules/backend/test/unit/IgfsService.test.js
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const assert = require('chai').assert;
+const injector = require('../injector');
+const testIgfss = require('../data/igfss.json');
+const testAccounts = require('../data/accounts.json');
+const testSpaces = require('../data/spaces.json');
+
+let igfsService;
+let mongo;
+let errors;
+let db;
+
+suite('IgfsServiceTestsSuite', () => {
+    suiteSetup(() => {
+        return Promise.all([injector('services/igfss'),
+            injector('mongo'),
+            injector('errors'),
+            injector('dbHelper')])
+            .then(([_igfsService, _mongo, _errors, _db]) => {
+                mongo = _mongo;
+                igfsService = _igfsService;
+                errors = _errors;
+                db = _db;
+            });
+    });
+
+    setup(() => db.init());
+
+    test('Create new igfs', (done) => {
+        const dupleIgfs = Object.assign({}, testIgfss[0], {name: 'Other name'});
+
+        delete dupleIgfs._id;
+
+        igfsService.merge(dupleIgfs)
+            .then((igfs) => {
+                assert.isNotNull(igfs._id);
+
+                return mongo.Igfs.findById(igfs._id);
+            })
+            .then((igfs) =>
+                assert.isNotNull(igfs)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('Update existed igfs', (done) => {
+        const newName = 'NewUniqueName';
+
+        const igfsBeforeMerge = Object.assign({}, testIgfss[0], {name: newName});
+
+        igfsService.merge(igfsBeforeMerge)
+            .then((igfs) => mongo.Igfs.findById(igfs._id))
+            .then((igfsAfterMerge) => assert.equal(igfsAfterMerge.name, newName))
+            .then(done)
+            .catch(done);
+    });
+
+    test('Create duplicated igfs', (done) => {
+        const dupleIfgs = Object.assign({}, testIgfss[0]);
+
+        delete dupleIfgs._id;
+
+        igfsService.merge(dupleIfgs)
+            .catch((err) => {
+                assert.instanceOf(err, errors.DuplicateKeyException);
+
+                done();
+            });
+    });
+
+    test('Remove existed igfs', (done) => {
+        igfsService.remove(testIgfss[0]._id)
+            .then(({rowsAffected}) => assert.equal(rowsAffected, 1))
+            .then(() => mongo.Igfs.findById(testIgfss[0]._id))
+            .then((notFoundIgfs) =>
+                assert.isNull(notFoundIgfs)
+            )
+            .then(done)
+            .catch(done);
+    });
+
+    test('Remove igfs without identifier', (done) => {
+        igfsService.remove()
+            .catch((err) => {
+                assert.instanceOf(err, errors.IllegalArgumentException);
+
+                done();
+            });
+    });
+
+    test('Remove missed igfs', (done) => {
+        const validNoExistingId = 'FFFFFFFFFFFFFFFFFFFFFFFF';
+
+        igfsService.remove(validNoExistingId)
+            .then(({rowsAffected}) => assert.equal(rowsAffected, 0))
+            .then(done)
+            .catch(done);
+    });
+
+    test('Get all igfss by space', (done) => {
+        igfsService.listBySpaces(testSpaces[0]._id)
+            .then((igfss) => assert.equal(igfss.length, 1))
+            .then(done)
+            .catch(done);
+    });
+
+    test('Remove all igfss in space', (done) => {
+        igfsService.removeAll(testAccounts[0]._id, false)
+            .then(({rowsAffected}) => assert.equal(rowsAffected, 1))
+            .then(done)
+            .catch(done);
+    });
+
+    test('Update linked entities on update igfs', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+
+    test('Update linked entities on remove igfs', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+
+    test('Update linked entities on remove all igfss in space', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+});
diff --git a/modules/backend/test/unit/Utils.test.js b/modules/backend/test/unit/Utils.test.js
new file mode 100644
index 0000000..10aa0d0
--- /dev/null
+++ b/modules/backend/test/unit/Utils.test.js
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const assert = require('chai').assert;
+const injector = require('../injector');
+
+let utils;
+let errors;
+let db;
+
+suite('UtilsTestsSuite', () => {
+    suiteSetup(() => {
+        return Promise.all([injector('services/utils'),
+            injector('errors'),
+            injector('dbHelper')])
+            .then(([_utils, _errors, _db]) => {
+                utils = _utils;
+                errors = _errors;
+                db = _db;
+            });
+    });
+
+    setup(() => db.init());
+
+    test('Check token generator', () => {
+        const tokenLength = 16;
+        const token1 = utils.randomString(tokenLength);
+        const token2 = utils.randomString(tokenLength);
+
+        assert.equal(token1.length, tokenLength);
+        assert.equal(token2.length, tokenLength);
+        assert.notEqual(token1, token2);
+    });
+});
diff --git a/modules/compatibility/README.txt b/modules/compatibility/README.txt
new file mode 100644
index 0000000..3b6734f
--- /dev/null
+++ b/modules/compatibility/README.txt
@@ -0,0 +1,3 @@
+Compatibility tests
+======================================
+Tests for compatibility between Ignite Configuration and Web Console Configuration Wizard.
diff --git a/modules/compatibility/pom.xml b/modules/compatibility/pom.xml
new file mode 100644
index 0000000..0b4826d
--- /dev/null
+++ b/modules/compatibility/pom.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<!--
+    POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.ignite</groupId>
+        <artifactId>ignite-parent</artifactId>
+        <version>1</version>
+        <relativePath>../../parent</relativePath>
+    </parent>
+
+    <artifactId>compatibility</artifactId>
+    <packaging>jar</packaging>
+    <version>2.10.0-SNAPSHOT</version>
+    <url>http://ignite.apache.org</url>
+
+    <properties>
+        <maven.build.timestamp.format>yyMMddHHmmss</maven.build.timestamp.format>
+    </properties>
+
+    <repositories>
+        <repository>
+            <id>GridGain External Repository</id>
+            <url>https://www.gridgainsystems.com/nexus/content/repositories/external/</url>
+        </repository>
+    </repositories>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-core</artifactId>
+            <version>${ignite.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-aws</artifactId>
+            <version>${ignite.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-hadoop</artifactId>
+            <version>${ignite.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-hibernate_5.1</artifactId>
+            <version>2.7.2</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-urideploy</artifactId>
+            <version>${ignite.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>ignite-web-agent-${project.version}</finalName>
+
+        <testResources>
+            <testResource>
+                <directory>src/test/java</directory>
+                <excludes>
+                    <exclude>**/*.java</exclude>
+                </excludes>
+            </testResource>
+            <testResource>
+                <directory>src/test/resources</directory>
+            </testResource>
+        </testResources>
+
+        <plugins>
+            <plugin>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>2.5</version>
+
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>org.apache.ignite.console.agent.AgentLauncher</mainClass>
+                        </manifest>
+                        <manifestEntries>
+                            <Build-Time>${maven.build.timestamp}</Build-Time>
+                        </manifestEntries>
+                    </archive>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>2.4</version>
+
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+
+                        <configuration>
+                            <createDependencyReducedPom>false</createDependencyReducedPom>
+                            <filters>
+                                <filter>
+                                    <artifact>*:*</artifact>
+                                    <excludes>
+                                        <exclude>META-INF/maven/**</exclude>
+                                    </excludes>
+                                </filter>
+                            </filters>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <version>2.4</version>
+                <inherited>false</inherited>
+
+                <executions>
+                    <execution>
+                        <id>release-web-agent</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <configuration>
+                            <descriptors>
+                                <descriptor>assembly/release-web-agent.xml</descriptor>
+                            </descriptors>
+                            <finalName>ignite-web-agent-${project.version}</finalName>
+                            <outputDirectory>target</outputDirectory>
+                            <appendAssemblyId>false</appendAssemblyId>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <skip>true</skip>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-deploy-plugin</artifactId>
+                <configuration>
+                    <skip>true</skip>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/modules/compatibility/src/test/java/org/apache/ignite/console/configuration/FieldProcessingInfo.java b/modules/compatibility/src/test/java/org/apache/ignite/console/configuration/FieldProcessingInfo.java
new file mode 100644
index 0000000..8637c99
--- /dev/null
+++ b/modules/compatibility/src/test/java/org/apache/ignite/console/configuration/FieldProcessingInfo.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.configuration;
+
+/**
+ * Service class with information about property in configuration state.
+ */
+public class FieldProcessingInfo {
+    /** Property name. */
+    private final String name;
+
+    /** Count of occurrence in configuration class. */
+    private int occurrence;
+
+    /** Deprecated sign of property getter or setter method. */
+    private boolean deprecated;
+
+    /**
+     * Constructor.
+     *
+     * @param name Property name.
+     * @param occurrence Count of occurrence in configuration class.
+     * @param deprecated Deprecated sign of property getter or setter method.
+     */
+    public FieldProcessingInfo(String name, int occurrence, boolean deprecated) {
+        this.name = name;
+        this.occurrence = occurrence;
+        this.deprecated = deprecated;
+    }
+
+    /**
+     * @return Property name.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * @return Count of occurrence in configuration class.
+     */
+    public int getOccurrence() {
+        return occurrence;
+    }
+
+    /**
+     * @return Deprecated sign of property getter or setter method.
+     */
+    public boolean isDeprecated() {
+        return deprecated;
+    }
+
+    /**
+     * Increase occurrence count.
+     *
+     * @return {@code this} for chaining.
+     */
+    public FieldProcessingInfo next() {
+        occurrence += 1;
+
+        return this;
+    }
+
+    /**
+     * Set deprecated state.
+     *
+     * @param state Deprecated state of checked method.
+     * @return {@code this} for chaining.
+     */
+    public FieldProcessingInfo deprecated(boolean state) {
+        deprecated = deprecated || state;
+
+        return this;
+    }
+}
diff --git a/modules/compatibility/src/test/java/org/apache/ignite/console/configuration/MetadataInfo.java b/modules/compatibility/src/test/java/org/apache/ignite/console/configuration/MetadataInfo.java
new file mode 100644
index 0000000..ff81c33
--- /dev/null
+++ b/modules/compatibility/src/test/java/org/apache/ignite/console/configuration/MetadataInfo.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.configuration;
+
+import java.util.Set;
+
+/**
+ * Service class with list of generated by configurator, known deprecated, and excluded from configurator fields.
+ */
+@SuppressWarnings("AssignmentOrReturnOfFieldWithMutableType")
+public class MetadataInfo {
+    /** List of generated fields. */
+    private final Set<String> generatedFields;
+
+    /** List of deprecated fields. */
+    private final Set<String> deprecatedFields;
+
+    /** List of excluded fields. */
+    private final Set<String> excludedFields;
+
+    /**
+     * Constructor.
+     *
+     * @param generatedFields List of generated fields.
+     * @param deprecatedFields List of deprecated fields.
+     * @param excludedFields List of excluded fields.
+     */
+    public MetadataInfo(Set<String> generatedFields, Set<String> deprecatedFields, Set<String> excludedFields) {
+        this.generatedFields = generatedFields;
+        this.deprecatedFields = deprecatedFields;
+        this.excludedFields = excludedFields;
+    }
+
+    /**
+     * @return List of generated fields.
+     */
+    public Set<String> getGeneratedFields() {
+        return generatedFields;
+    }
+
+    /**
+     * @return List of deprecated fields.
+     */
+    public Set<String> getDeprecatedFields() {
+        return deprecatedFields;
+    }
+
+    /**
+     * @return List of excluded fields.
+     */
+    public Set<String> getExcludedFields() {
+        return excludedFields;
+    }
+}
diff --git a/modules/compatibility/src/test/java/org/apache/ignite/console/configuration/WebConsoleConfigurationSelfTest.java b/modules/compatibility/src/test/java/org/apache/ignite/console/configuration/WebConsoleConfigurationSelfTest.java
new file mode 100644
index 0000000..5a72fc2
--- /dev/null
+++ b/modules/compatibility/src/test/java/org/apache/ignite/console/configuration/WebConsoleConfigurationSelfTest.java
@@ -0,0 +1,1117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.configuration;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.binary.BinaryTypeConfiguration;
+import org.apache.ignite.cache.CacheKeyConfiguration;
+import org.apache.ignite.cache.QueryEntity;
+import org.apache.ignite.cache.QueryIndex;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.cache.eviction.fifo.FifoEvictionPolicy;
+import org.apache.ignite.cache.eviction.lru.LruEvictionPolicy;
+import org.apache.ignite.cache.eviction.sorted.SortedEvictionPolicy;
+import org.apache.ignite.cache.store.hibernate.CacheHibernateBlobStore;
+import org.apache.ignite.cache.store.jdbc.CacheJdbcBlobStore;
+import org.apache.ignite.cache.store.jdbc.CacheJdbcPojoStoreFactory;
+import org.apache.ignite.cache.store.jdbc.JdbcType;
+import org.apache.ignite.configuration.AtomicConfiguration;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.ClientConnectorConfiguration;
+import org.apache.ignite.configuration.ConnectorConfiguration;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.ExecutorConfiguration;
+import org.apache.ignite.configuration.FileSystemConfiguration;
+import org.apache.ignite.configuration.HadoopConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.configuration.MemoryConfiguration;
+import org.apache.ignite.configuration.MemoryPolicyConfiguration;
+import org.apache.ignite.configuration.NearCacheConfiguration;
+import org.apache.ignite.configuration.OdbcConfiguration;
+import org.apache.ignite.configuration.PersistentStoreConfiguration;
+import org.apache.ignite.configuration.SqlConnectorConfiguration;
+import org.apache.ignite.configuration.TransactionConfiguration;
+import org.apache.ignite.hadoop.fs.CachingHadoopFileSystemFactory;
+import org.apache.ignite.hadoop.fs.IgniteHadoopIgfsSecondaryFileSystem;
+import org.apache.ignite.hadoop.fs.KerberosHadoopFileSystemFactory;
+import org.apache.ignite.hadoop.mapreduce.IgniteHadoopWeightedMapReducePlanner;
+import org.apache.ignite.hadoop.util.BasicUserNameMapper;
+import org.apache.ignite.hadoop.util.ChainedUserNameMapper;
+import org.apache.ignite.hadoop.util.KerberosUserNameMapper;
+import org.apache.ignite.igfs.IgfsGroupDataBlocksKeyMapper;
+import org.apache.ignite.igfs.IgfsIpcEndpointConfiguration;
+import org.apache.ignite.internal.marshaller.optimized.OptimizedMarshaller;
+import org.apache.ignite.services.ServiceConfiguration;
+import org.apache.ignite.spi.checkpoint.cache.CacheCheckpointSpi;
+import org.apache.ignite.spi.checkpoint.jdbc.JdbcCheckpointSpi;
+import org.apache.ignite.spi.checkpoint.s3.S3CheckpointSpi;
+import org.apache.ignite.spi.checkpoint.sharedfs.SharedFsCheckpointSpi;
+import org.apache.ignite.spi.collision.fifoqueue.FifoQueueCollisionSpi;
+import org.apache.ignite.spi.collision.jobstealing.JobStealingCollisionSpi;
+import org.apache.ignite.spi.collision.priorityqueue.PriorityQueueCollisionSpi;
+import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi;
+import org.apache.ignite.spi.deployment.local.LocalDeploymentSpi;
+import org.apache.ignite.spi.deployment.uri.UriDeploymentSpi;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.encryption.keystore.KeystoreEncryptionSpi;
+import org.apache.ignite.spi.eventstorage.memory.MemoryEventStorageSpi;
+import org.apache.ignite.spi.failover.always.AlwaysFailoverSpi;
+import org.apache.ignite.spi.failover.jobstealing.JobStealingFailoverSpi;
+import org.apache.ignite.spi.loadbalancing.adaptive.AdaptiveCpuLoadProbe;
+import org.apache.ignite.spi.loadbalancing.adaptive.AdaptiveJobCountLoadProbe;
+import org.apache.ignite.spi.loadbalancing.adaptive.AdaptiveLoadBalancingSpi;
+import org.apache.ignite.spi.loadbalancing.adaptive.AdaptiveProcessingTimeLoadProbe;
+import org.apache.ignite.spi.loadbalancing.roundrobin.RoundRobinLoadBalancingSpi;
+import org.apache.ignite.spi.loadbalancing.weightedrandom.WeightedRandomLoadBalancingSpi;
+import org.apache.ignite.ssl.SslContextFactory;
+import org.junit.Test;
+
+/**
+ * Check difference of Ignite configuration with Ignite Web Console "Configuration" screen.
+ */
+public class WebConsoleConfigurationSelfTest {
+    /** */
+    protected static final Set<String> EMPTY_FIELDS = Collections.emptySet();
+
+    /** */
+    protected static final Set<String> SPI_EXCLUDED_FIELDS = Collections.singleton("name");
+
+    /** Map of properties metadata by class. */
+    protected final Map<Class<?>, MetadataInfo> metadata = new HashMap<>();
+
+    /**
+     * @param msg Message to log.
+     */
+    protected void log(String msg) {
+        System.out.println(msg);
+    }
+
+    /**
+     * Prepare metadata for properties, which are possible to configure.
+     */
+    @SuppressWarnings("deprecation")
+    protected void prepareMetadata() {
+        // Cluster configuration.
+        Set<String> igniteCfgProps = new HashSet<>();
+        igniteCfgProps.add("cacheConfiguration");
+        igniteCfgProps.add("discoverySpi");
+        igniteCfgProps.add("localHost");
+        igniteCfgProps.add("atomicConfiguration");
+        igniteCfgProps.add("userAttributes");
+        igniteCfgProps.add("binaryConfiguration");
+        igniteCfgProps.add("cacheKeyConfiguration");
+        igniteCfgProps.add("checkpointSpi");
+        igniteCfgProps.add("collisionSpi");
+        igniteCfgProps.add("communicationSpi");
+        igniteCfgProps.add("networkTimeout");
+        igniteCfgProps.add("networkSendRetryDelay");
+        igniteCfgProps.add("networkSendRetryCount");
+        igniteCfgProps.add("connectorConfiguration");
+        igniteCfgProps.add("dataStorageConfiguration");
+        igniteCfgProps.add("deploymentMode");
+        igniteCfgProps.add("peerClassLoadingEnabled");
+        igniteCfgProps.add("peerClassLoadingMissedResourcesCacheSize");
+        igniteCfgProps.add("peerClassLoadingThreadPoolSize");
+        igniteCfgProps.add("peerClassLoadingLocalClassPathExclude");
+        igniteCfgProps.add("classLoader");
+        igniteCfgProps.add("deploymentSpi");
+        igniteCfgProps.add("eventStorageSpi");
+        igniteCfgProps.add("includeEventTypes");
+        igniteCfgProps.add("failureDetectionTimeout");
+        igniteCfgProps.add("clientFailureDetectionTimeout");
+        igniteCfgProps.add("failoverSpi");
+        igniteCfgProps.add("hadoopConfiguration");
+        igniteCfgProps.add("loadBalancingSpi");
+        igniteCfgProps.add("marshalLocalJobs");
+
+        // Removed since 2.0.
+        // igniteCfgProps.add("marshallerCacheKeepAliveTime");
+        // igniteCfgProps.add("marshallerCacheThreadPoolSize");
+
+        igniteCfgProps.add("metricsExpireTime");
+        igniteCfgProps.add("metricsHistorySize");
+        igniteCfgProps.add("metricsLogFrequency");
+        igniteCfgProps.add("metricsUpdateFrequency");
+        igniteCfgProps.add("workDirectory");
+        igniteCfgProps.add("consistentId");
+        igniteCfgProps.add("warmupClosure");
+        igniteCfgProps.add("activeOnStart");
+        igniteCfgProps.add("cacheSanityCheckEnabled");
+        igniteCfgProps.add("longQueryWarningTimeout");
+        igniteCfgProps.add("odbcConfiguration");
+        igniteCfgProps.add("serviceConfiguration");
+        igniteCfgProps.add("sqlConnectorConfiguration");
+        igniteCfgProps.add("sslContextFactory");
+
+        // Removed since 2.0.
+        // igniteCfgProps.add("swapSpaceSpi");
+
+        igniteCfgProps.add("publicThreadPoolSize");
+        igniteCfgProps.add("systemThreadPoolSize");
+        igniteCfgProps.add("serviceThreadPoolSize");
+        igniteCfgProps.add("managementThreadPoolSize");
+        igniteCfgProps.add("igfsThreadPoolSize");
+        igniteCfgProps.add("utilityCacheThreadPoolSize");
+        igniteCfgProps.add("utilityCacheKeepAliveTime");
+        igniteCfgProps.add("asyncCallbackPoolSize");
+        igniteCfgProps.add("stripedPoolSize");
+        igniteCfgProps.add("dataStreamerThreadPoolSize");
+        igniteCfgProps.add("queryThreadPoolSize");
+        igniteCfgProps.add("executorConfiguration");
+
+        // Removed since 2.0.
+        // igniteCfgProps.add("clockSyncSamples");
+        // igniteCfgProps.add("clockSyncFrequency");
+
+        igniteCfgProps.add("timeServerPortBase");
+        igniteCfgProps.add("timeServerPortRange");
+        igniteCfgProps.add("transactionConfiguration");
+        igniteCfgProps.add("clientConnectorConfiguration");
+        igniteCfgProps.add("fileSystemConfiguration");
+        igniteCfgProps.add("gridLogger");
+        igniteCfgProps.add("pluginConfigurations");
+        igniteCfgProps.add("mvccVacuumFrequency");
+        igniteCfgProps.add("mvccVacuumThreadCount");
+        igniteCfgProps.add("encryptionSpi");
+        igniteCfgProps.add("authenticationEnabled");
+        igniteCfgProps.add("sqlQueryHistorySize");
+        igniteCfgProps.add("lifecycleBeans");
+        igniteCfgProps.add("addressResolver");
+        igniteCfgProps.add("mBeanServer");
+        igniteCfgProps.add("networkCompressionLevel");
+        igniteCfgProps.add("systemWorkerBlockedTimeout");
+        igniteCfgProps.add("includeProperties");
+        igniteCfgProps.add("cacheStoreSessionListenerFactories");
+        igniteCfgProps.add("sqlSchemas");
+        igniteCfgProps.add("igniteInstanceName");
+        igniteCfgProps.add("communicationFailureResolver");
+        igniteCfgProps.add("failureHandler");
+        igniteCfgProps.add("rebalanceThreadPoolSize");
+        igniteCfgProps.add("localEventListeners");
+
+        Set<String> igniteCfgPropsDep = new HashSet<>();
+        igniteCfgPropsDep.add("gridName");
+        igniteCfgPropsDep.add("lateAffinityAssignment");
+        igniteCfgPropsDep.add("persistentStoreConfiguration");
+        igniteCfgPropsDep.add("memoryConfiguration");
+        igniteCfgPropsDep.add("marshaller");
+        igniteCfgPropsDep.add("discoveryStartupDelay");
+
+        Set<String> igniteCfgPropsExcl = new HashSet<>();
+        // igniteCfgPropsExcl.add("lifecycleBeans");
+        igniteCfgPropsExcl.add("daemon");
+        igniteCfgPropsExcl.add("clientMode");
+        igniteCfgPropsExcl.add("indexingSpi");
+        igniteCfgPropsExcl.add("nodeId");
+        igniteCfgPropsExcl.add("platformConfiguration");
+        igniteCfgPropsExcl.add("segmentCheckFrequency");
+        igniteCfgPropsExcl.add("allSegmentationResolversPassRequired");
+        igniteCfgPropsExcl.add("segmentationPolicy");
+        igniteCfgPropsExcl.add("segmentationResolveAttempts");
+        igniteCfgPropsExcl.add("waitForSegmentOnStart");
+        igniteCfgPropsExcl.add("segmentationResolvers");
+        igniteCfgPropsExcl.add("autoActivationEnabled");
+        igniteCfgPropsExcl.add("igniteHome");
+
+        metadata.put(IgniteConfiguration.class,
+            new MetadataInfo(igniteCfgProps, igniteCfgPropsDep, igniteCfgPropsExcl));
+
+        Set<String> encriptionSpiProps = new HashSet<>();
+        encriptionSpiProps.add("keySize");
+        encriptionSpiProps.add("masterKeyName");
+        encriptionSpiProps.add("keyStorePath");
+        metadata.put(KeystoreEncryptionSpi.class, new MetadataInfo(encriptionSpiProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> cacheKeyCfgProps = new HashSet<>();
+        cacheKeyCfgProps.add("typeName");
+        cacheKeyCfgProps.add("affinityKeyFieldName");
+
+        metadata.put(CacheKeyConfiguration.class, new MetadataInfo(cacheKeyCfgProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> atomicCfgProps = new HashSet<>();
+        atomicCfgProps.add("cacheMode");
+        atomicCfgProps.add("atomicSequenceReserveSize");
+        atomicCfgProps.add("backups");
+        atomicCfgProps.add("affinity");
+        atomicCfgProps.add("groupName");
+
+        metadata.put(AtomicConfiguration.class, new MetadataInfo(atomicCfgProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> binaryCfgProps = new HashSet<>();
+        binaryCfgProps.add("idMapper");
+        binaryCfgProps.add("nameMapper");
+        binaryCfgProps.add("serializer");
+        binaryCfgProps.add("typeConfigurations");
+        binaryCfgProps.add("compactFooter");
+        metadata.put(BinaryConfiguration.class, new MetadataInfo(binaryCfgProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> binaryTypeCfgProps = new HashSet<>();
+        binaryTypeCfgProps.add("typeName");
+        binaryTypeCfgProps.add("idMapper");
+        binaryTypeCfgProps.add("nameMapper");
+        binaryTypeCfgProps.add("serializer");
+        binaryTypeCfgProps.add("enum");
+        binaryTypeCfgProps.add("enumValues");
+        metadata.put(BinaryTypeConfiguration.class, new MetadataInfo(binaryTypeCfgProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> sharedFsCheckpointProps = new HashSet<>();
+        sharedFsCheckpointProps.add("directoryPaths");
+        metadata.put(SharedFsCheckpointSpi.class,
+            new MetadataInfo(sharedFsCheckpointProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> s3CheckpointProps = new HashSet<>();
+        s3CheckpointProps.add("bucketNameSuffix");
+        s3CheckpointProps.add("bucketEndpoint");
+        s3CheckpointProps.add("sSEAlgorithm");
+        s3CheckpointProps.add("checkpointListener");
+        metadata.put(S3CheckpointSpi.class, new MetadataInfo(s3CheckpointProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> cacheCheckpointProps = new HashSet<>();
+        cacheCheckpointProps.add("cacheName");
+        metadata.put(CacheCheckpointSpi.class, new MetadataInfo(cacheCheckpointProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> jdbcCheckpointProps = new HashSet<>();
+        // Only setter for dataSource.
+        // jdbcCheckpointProps.add("dataSourceBean");
+        // jdbcCheckpointProps.add("dialect");
+        jdbcCheckpointProps.add("checkpointListener");
+        jdbcCheckpointProps.add("user");
+        // Only on code generation.
+        jdbcCheckpointProps.add("pwd");
+        jdbcCheckpointProps.add("checkpointTableName");
+        jdbcCheckpointProps.add("numberOfRetries");
+        jdbcCheckpointProps.add("keyFieldName");
+        jdbcCheckpointProps.add("keyFieldType");
+        jdbcCheckpointProps.add("valueFieldName");
+        jdbcCheckpointProps.add("valueFieldType");
+        jdbcCheckpointProps.add("expireDateFieldName");
+        jdbcCheckpointProps.add("expireDateFieldType");
+        metadata.put(JdbcCheckpointSpi.class, new MetadataInfo(jdbcCheckpointProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> cliConProps = new HashSet<>();
+        cliConProps.add("host");
+        cliConProps.add("port");
+        cliConProps.add("portRange");
+        cliConProps.add("socketSendBufferSize");
+        cliConProps.add("socketReceiveBufferSize");
+        cliConProps.add("maxOpenCursorsPerConnection");
+        cliConProps.add("threadPoolSize");
+        cliConProps.add("tcpNoDelay");
+        cliConProps.add("idleTimeout");
+        cliConProps.add("sslEnabled");
+        cliConProps.add("sslClientAuth");
+        cliConProps.add("useIgniteSslContextFactory");
+        cliConProps.add("sslContextFactory");
+        cliConProps.add("jdbcEnabled");
+        cliConProps.add("odbcEnabled");
+        cliConProps.add("thinClientEnabled");
+        metadata.put(ClientConnectorConfiguration.class, new MetadataInfo(cliConProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> jobStealingCollisionProps = new HashSet<>();
+        jobStealingCollisionProps.add("activeJobsThreshold");
+        jobStealingCollisionProps.add("waitJobsThreshold");
+        jobStealingCollisionProps.add("messageExpireTime");
+        jobStealingCollisionProps.add("maximumStealingAttempts");
+        jobStealingCollisionProps.add("stealingEnabled");
+        jobStealingCollisionProps.add("externalCollisionListener");
+        jobStealingCollisionProps.add("stealingAttributes");
+        metadata.put(JobStealingCollisionSpi.class,
+            new MetadataInfo(jobStealingCollisionProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> priQueueCollisionProps = new HashSet<>();
+        priQueueCollisionProps.add("parallelJobsNumber");
+        priQueueCollisionProps.add("waitingJobsNumber");
+        priQueueCollisionProps.add("priorityAttributeKey");
+        priQueueCollisionProps.add("jobPriorityAttributeKey");
+        priQueueCollisionProps.add("defaultPriority");
+        priQueueCollisionProps.add("starvationIncrement");
+        priQueueCollisionProps.add("starvationPreventionEnabled");
+        metadata.put(PriorityQueueCollisionSpi.class, new MetadataInfo(priQueueCollisionProps, EMPTY_FIELDS,
+            SPI_EXCLUDED_FIELDS));
+
+        Set<String> fifoQueueCollisionProps = new HashSet<>();
+        fifoQueueCollisionProps.add("parallelJobsNumber");
+        fifoQueueCollisionProps.add("waitingJobsNumber");
+        metadata.put(FifoQueueCollisionSpi.class,
+            new MetadataInfo(fifoQueueCollisionProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> commProps = new HashSet<>();
+        commProps.add("listener");
+        commProps.add("localAddress");
+        commProps.add("localPort");
+        commProps.add("localPortRange");
+        commProps.add("sharedMemoryPort");
+        commProps.add("idleConnectionTimeout");
+        commProps.add("connectTimeout");
+        commProps.add("maxConnectTimeout");
+        commProps.add("reconnectCount");
+        commProps.add("socketSendBuffer");
+        commProps.add("socketReceiveBuffer");
+        commProps.add("slowClientQueueLimit");
+        commProps.add("ackSendThreshold");
+        commProps.add("messageQueueLimit");
+        commProps.add("unacknowledgedMessagesBufferSize");
+        commProps.add("socketWriteTimeout");
+        commProps.add("selectorsCount");
+        commProps.add("addressResolver");
+        commProps.add("directBuffer");
+        commProps.add("directSendBuffer");
+        commProps.add("tcpNoDelay");
+        commProps.add("selectorSpins");
+        commProps.add("connectionsPerNode");
+        commProps.add("usePairedConnections");
+        commProps.add("filterReachableAddresses");
+
+        metadata.put(TcpCommunicationSpi.class, new MetadataInfo(commProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> discoverySpiProps = new HashSet<>();
+        discoverySpiProps.add("ipFinder");
+        discoverySpiProps.add("localAddress");
+        discoverySpiProps.add("localPort");
+        discoverySpiProps.add("localPortRange");
+        discoverySpiProps.add("addressResolver");
+        discoverySpiProps.add("socketTimeout");
+        discoverySpiProps.add("ackTimeout");
+        discoverySpiProps.add("maxAckTimeout");
+        discoverySpiProps.add("networkTimeout");
+        discoverySpiProps.add("joinTimeout");
+        discoverySpiProps.add("threadPriority");
+        // Removed since 2.0.
+        // discoverySpiProps.add("heartbeatFrequency");
+        // discoverySpiProps.add("maxMissedHeartbeats");
+        // discoverySpiProps.add("maxMissedClientHeartbeats");
+        discoverySpiProps.add("topHistorySize");
+        discoverySpiProps.add("listener");
+        discoverySpiProps.add("dataExchange");
+        discoverySpiProps.add("metricsProvider");
+        discoverySpiProps.add("reconnectCount");
+        discoverySpiProps.add("statisticsPrintFrequency");
+        discoverySpiProps.add("ipFinderCleanFrequency");
+        discoverySpiProps.add("authenticator");
+        discoverySpiProps.add("forceServerMode");
+        discoverySpiProps.add("clientReconnectDisabled");
+        discoverySpiProps.add("connectionRecoveryTimeout");
+        discoverySpiProps.add("reconnectDelay");
+        discoverySpiProps.add("soLinger");
+
+        Set<String> discoverySpiExclProps = new HashSet<>();
+        discoverySpiExclProps.addAll(SPI_EXCLUDED_FIELDS);
+        discoverySpiExclProps.add("nodeAttributes");
+        metadata.put(TcpDiscoverySpi.class, new MetadataInfo(discoverySpiProps, EMPTY_FIELDS, discoverySpiExclProps));
+
+        Set<String> connectorProps = new HashSet<>();
+        connectorProps.add("jettyPath");
+        connectorProps.add("host");
+        connectorProps.add("port");
+        connectorProps.add("portRange");
+        connectorProps.add("idleQueryCursorTimeout");
+        connectorProps.add("idleQueryCursorCheckFrequency");
+        connectorProps.add("idleTimeout");
+        connectorProps.add("receiveBufferSize");
+        connectorProps.add("sendBufferSize");
+        connectorProps.add("sendQueueLimit");
+        connectorProps.add("directBuffer");
+        connectorProps.add("noDelay");
+        connectorProps.add("selectorCount");
+        connectorProps.add("threadPoolSize");
+        connectorProps.add("messageInterceptor");
+        connectorProps.add("secretKey");
+        connectorProps.add("sslEnabled");
+        connectorProps.add("sslClientAuth");
+        connectorProps.add("sslFactory");
+
+        Set<String> connectorPropsDep = new HashSet<>();
+        connectorPropsDep.add("sslContextFactory");
+        metadata.put(ConnectorConfiguration.class, new MetadataInfo(connectorProps, connectorPropsDep, EMPTY_FIELDS));
+
+        Set<String> dataStorageProps = new HashSet<>();
+        dataStorageProps.add("pageSize");
+        dataStorageProps.add("concurrencyLevel");
+        dataStorageProps.add("systemRegionInitialSize");
+        dataStorageProps.add("systemRegionMaxSize");
+        dataStorageProps.add("defaultDataRegionConfiguration");
+        dataStorageProps.add("dataRegionConfigurations");
+        dataStorageProps.add("storagePath");
+        dataStorageProps.add("checkpointFrequency");
+        dataStorageProps.add("checkpointThreads");
+        dataStorageProps.add("checkpointWriteOrder");
+        dataStorageProps.add("walMode");
+        dataStorageProps.add("walPath");
+        dataStorageProps.add("walArchivePath");
+        dataStorageProps.add("walSegments");
+        dataStorageProps.add("walSegmentSize");
+        dataStorageProps.add("walHistorySize");
+        dataStorageProps.add("walBufferSize");
+        dataStorageProps.add("walFlushFrequency");
+        dataStorageProps.add("walFsyncDelayNanos");
+        dataStorageProps.add("walRecordIteratorBufferSize");
+        dataStorageProps.add("lockWaitTime");
+        dataStorageProps.add("walThreadLocalBufferSize");
+        dataStorageProps.add("metricsSubIntervalCount");
+        dataStorageProps.add("metricsRateTimeInterval");
+        dataStorageProps.add("fileIOFactory");
+        dataStorageProps.add("walAutoArchiveAfterInactivity");
+        dataStorageProps.add("metricsEnabled");
+        dataStorageProps.add("alwaysWriteFullPages");
+        dataStorageProps.add("writeThrottlingEnabled");
+        dataStorageProps.add("checkpointReadLockTimeout");
+        dataStorageProps.add("maxWalArchiveSize");
+        dataStorageProps.add("walCompactionEnabled");
+        dataStorageProps.add("walCompactionLevel");
+        metadata.put(DataStorageConfiguration.class, new MetadataInfo(dataStorageProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> dataRegionProps = new HashSet<>();
+        dataRegionProps.add("name");
+        dataRegionProps.add("initialSize");
+        dataRegionProps.add("maxSize");
+        dataRegionProps.add("swapPath");
+        dataRegionProps.add("checkpointPageBufferSize");
+        dataRegionProps.add("pageEvictionMode");
+        dataRegionProps.add("evictionThreshold");
+        dataRegionProps.add("emptyPagesPoolSize");
+        dataRegionProps.add("metricsSubIntervalCount");
+        dataRegionProps.add("metricsRateTimeInterval");
+        dataRegionProps.add("metricsEnabled");
+        dataRegionProps.add("persistenceEnabled");
+        metadata.put(DataRegionConfiguration.class, new MetadataInfo(dataRegionProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> uriDeploymentProps = new HashSet<>();
+        uriDeploymentProps.add("uriList");
+        uriDeploymentProps.add("temporaryDirectoryPath");
+        uriDeploymentProps.add("scanners");
+        uriDeploymentProps.add("listener");
+        uriDeploymentProps.add("checkMd5");
+        uriDeploymentProps.add("encodeUri");
+        metadata.put(UriDeploymentSpi.class, new MetadataInfo(uriDeploymentProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> locDeploymentProps = new HashSet<>();
+        locDeploymentProps.add("listener");
+        metadata.put(LocalDeploymentSpi.class, new MetadataInfo(locDeploymentProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> memoryEvtStorageProps = new HashSet<>();
+        memoryEvtStorageProps.add("expireAgeMs");
+        memoryEvtStorageProps.add("expireCount");
+        memoryEvtStorageProps.add("filter");
+        metadata.put(MemoryEventStorageSpi.class,
+            new MetadataInfo(memoryEvtStorageProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> alwaysFailoverProps = new HashSet<>();
+        alwaysFailoverProps.add("maximumFailoverAttempts");
+        metadata.put(AlwaysFailoverSpi.class, new MetadataInfo(alwaysFailoverProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> jobStealingFailoverProps = new HashSet<>();
+        jobStealingFailoverProps.add("maximumFailoverAttempts");
+        metadata.put(JobStealingFailoverSpi.class,
+            new MetadataInfo(jobStealingFailoverProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> hadoopCfgProps = new HashSet<>();
+        hadoopCfgProps.add("mapReducePlanner");
+        hadoopCfgProps.add("finishedJobInfoTtl");
+        hadoopCfgProps.add("maxParallelTasks");
+        hadoopCfgProps.add("maxTaskQueueSize");
+        hadoopCfgProps.add("nativeLibraryNames");
+        metadata.put(HadoopConfiguration.class, new MetadataInfo(hadoopCfgProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> hadoopWeightMapReduceCfgProps = new HashSet<>();
+        hadoopWeightMapReduceCfgProps.add("localMapperWeight");
+        hadoopWeightMapReduceCfgProps.add("remoteMapperWeight");
+        hadoopWeightMapReduceCfgProps.add("localReducerWeight");
+        hadoopWeightMapReduceCfgProps.add("remoteReducerWeight");
+        hadoopWeightMapReduceCfgProps.add("preferLocalReducerThresholdWeight");
+        metadata.put(IgniteHadoopWeightedMapReducePlanner.class,
+            new MetadataInfo(hadoopWeightMapReduceCfgProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> weightedRndLoadBalancingProps = new HashSet<>();
+        weightedRndLoadBalancingProps.add("nodeWeight");
+        weightedRndLoadBalancingProps.add("useWeights");
+        metadata.put(WeightedRandomLoadBalancingSpi.class,
+            new MetadataInfo(weightedRndLoadBalancingProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> adaptiveLoadBalancingProps = new HashSet<>();
+        adaptiveLoadBalancingProps.add("loadProbe");
+        metadata.put(AdaptiveLoadBalancingSpi.class,
+            new MetadataInfo(adaptiveLoadBalancingProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> roundRobinLoadBalancingProps = new HashSet<>();
+        roundRobinLoadBalancingProps.add("perTask");
+        metadata.put(RoundRobinLoadBalancingSpi.class,
+            new MetadataInfo(roundRobinLoadBalancingProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> jobCntProbeProps = new HashSet<>();
+        jobCntProbeProps.add("useAverage");
+        metadata.put(AdaptiveJobCountLoadProbe.class,
+            new MetadataInfo(jobCntProbeProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> cpuLoadProbeProps = new HashSet<>();
+        cpuLoadProbeProps.add("useAverage");
+        cpuLoadProbeProps.add("useProcessors");
+        cpuLoadProbeProps.add("processorCoefficient");
+        metadata.put(AdaptiveCpuLoadProbe.class, new MetadataInfo(cpuLoadProbeProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> adaptiveTimeProbeProps = new HashSet<>();
+        adaptiveTimeProbeProps.add("useAverage");
+        metadata.put(AdaptiveProcessingTimeLoadProbe.class,
+            new MetadataInfo(adaptiveTimeProbeProps, EMPTY_FIELDS, SPI_EXCLUDED_FIELDS));
+
+        Set<String> optimizedMarshallerProps = new HashSet<>();
+        optimizedMarshallerProps.add("poolSize");
+        optimizedMarshallerProps.add("requireSerializable");
+
+        Set<String> optimizedMarshallerPropsExcl = new HashSet<>();
+        optimizedMarshallerPropsExcl.add("context");
+
+        metadata.put(OptimizedMarshaller.class,
+            new MetadataInfo(optimizedMarshallerProps, EMPTY_FIELDS, optimizedMarshallerPropsExcl));
+
+        Set<String> memoryCfgProps = new HashSet<>();
+        memoryCfgProps.add("pageSize");
+        memoryCfgProps.add("concurrencyLevel");
+        memoryCfgProps.add("systemCacheInitialSize");
+        memoryCfgProps.add("systemCacheMaxSize");
+        memoryCfgProps.add("defaultMemoryPolicyName");
+        memoryCfgProps.add("defaultMemoryPolicySize");
+        memoryCfgProps.add("memoryPolicies");
+        metadata.put(MemoryConfiguration.class, new MetadataInfo(EMPTY_FIELDS, memoryCfgProps, EMPTY_FIELDS));
+
+        Set<String> memoryPlcCfgProps = new HashSet<>();
+        memoryPlcCfgProps.add("name");
+        memoryPlcCfgProps.add("initialSize");
+        memoryPlcCfgProps.add("maxSize");
+        memoryPlcCfgProps.add("swapFilePath");
+        memoryPlcCfgProps.add("pageEvictionMode");
+        memoryPlcCfgProps.add("evictionThreshold");
+        memoryPlcCfgProps.add("emptyPagesPoolSize");
+        memoryPlcCfgProps.add("subIntervals");
+        memoryPlcCfgProps.add("rateTimeInterval");
+        memoryPlcCfgProps.add("metricsEnabled");
+        metadata.put(MemoryPolicyConfiguration.class, new MetadataInfo(EMPTY_FIELDS, memoryPlcCfgProps, EMPTY_FIELDS));
+
+        Set<String> odbcCfgProps = new HashSet<>();
+        odbcCfgProps.add("endpointAddress");
+        odbcCfgProps.add("socketSendBufferSize");
+        odbcCfgProps.add("socketReceiveBufferSize");
+        odbcCfgProps.add("maxOpenCursors");
+        odbcCfgProps.add("threadPoolSize");
+        metadata.put(OdbcConfiguration.class, new MetadataInfo(EMPTY_FIELDS, odbcCfgProps, EMPTY_FIELDS));
+
+        Set<String> persistenceCfgProps = new HashSet<>();
+        persistenceCfgProps.add("persistentStorePath");
+        persistenceCfgProps.add("metricsEnabled");
+        persistenceCfgProps.add("alwaysWriteFullPages");
+        persistenceCfgProps.add("checkpointingFrequency");
+        persistenceCfgProps.add("checkpointingPageBufferSize");
+        persistenceCfgProps.add("checkpointingThreads");
+        persistenceCfgProps.add("walStorePath");
+        persistenceCfgProps.add("walArchivePath");
+        persistenceCfgProps.add("walSegments");
+        persistenceCfgProps.add("walSegmentSize");
+        persistenceCfgProps.add("walHistorySize");
+        persistenceCfgProps.add("walFlushFrequency");
+        persistenceCfgProps.add("walFsyncDelayNanos");
+        persistenceCfgProps.add("walRecordIteratorBufferSize");
+        persistenceCfgProps.add("lockWaitTime");
+        persistenceCfgProps.add("rateTimeInterval");
+        persistenceCfgProps.add("tlbSize");
+        persistenceCfgProps.add("subIntervals");
+        persistenceCfgProps.add("walMode");
+        persistenceCfgProps.add("walAutoArchiveAfterInactivity");
+        persistenceCfgProps.add("writeThrottlingEnabled");
+        persistenceCfgProps.add("checkpointWriteOrder");
+        persistenceCfgProps.add("fileIOFactory");
+        persistenceCfgProps.add("walBufferSize");
+        metadata.put(PersistentStoreConfiguration.class,
+            new MetadataInfo(EMPTY_FIELDS, persistenceCfgProps, EMPTY_FIELDS));
+
+        Set<String> srvcCfgProps = new HashSet<>();
+        srvcCfgProps.add("name");
+        srvcCfgProps.add("service");
+        srvcCfgProps.add("maxPerNodeCount");
+        srvcCfgProps.add("totalCount");
+        // Field cache in model.
+        srvcCfgProps.add("cacheName");
+        srvcCfgProps.add("affinityKey");
+
+        Set<String> srvcCfgPropsExclude = new HashSet<>();
+        srvcCfgPropsExclude.add("nodeFilter");
+
+        metadata.put(ServiceConfiguration.class, new MetadataInfo(srvcCfgProps, EMPTY_FIELDS, srvcCfgPropsExclude));
+
+        Set<String> sqlConnectorCfgProps = new HashSet<>();
+        sqlConnectorCfgProps.add("host");
+        sqlConnectorCfgProps.add("port");
+        sqlConnectorCfgProps.add("portRange");
+        sqlConnectorCfgProps.add("socketSendBufferSize");
+        sqlConnectorCfgProps.add("socketReceiveBufferSize");
+        sqlConnectorCfgProps.add("maxOpenCursorsPerConnection");
+        sqlConnectorCfgProps.add("threadPoolSize");
+        sqlConnectorCfgProps.add("tcpNoDelay");
+        metadata.put(SqlConnectorConfiguration.class,
+            new MetadataInfo(EMPTY_FIELDS, sqlConnectorCfgProps, EMPTY_FIELDS));
+
+        Set<String> sslCfgProps = new HashSet<>();
+        sslCfgProps.add("keyAlgorithm");
+        sslCfgProps.add("keyStoreFilePath");
+        // Only on code generation.
+        sslCfgProps.add("keyStorePassword");
+        sslCfgProps.add("keyStoreType");
+        sslCfgProps.add("protocol");
+        sslCfgProps.add("trustManagers");
+        sslCfgProps.add("trustStoreFilePath");
+        // Only on code generation.
+        sslCfgProps.add("trustStorePassword");
+        sslCfgProps.add("trustStoreType");
+        sslCfgProps.add("cipherSuites");
+        sslCfgProps.add("protocols");
+        metadata.put(SslContextFactory.class, new MetadataInfo(sslCfgProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> executorProps = new HashSet<>();
+        executorProps.add("name");
+        executorProps.add("size");
+        metadata.put(ExecutorConfiguration.class, new MetadataInfo(executorProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> transactionCfgProps = new HashSet<>();
+        transactionCfgProps.add("defaultTxConcurrency");
+        transactionCfgProps.add("defaultTxIsolation");
+        transactionCfgProps.add("defaultTxTimeout");
+        transactionCfgProps.add("pessimisticTxLogLinger");
+        transactionCfgProps.add("pessimisticTxLogSize");
+        transactionCfgProps.add("txManagerFactory");
+        transactionCfgProps.add("deadlockTimeout");
+        transactionCfgProps.add("useJtaSynchronization");
+        transactionCfgProps.add("txTimeoutOnPartitionMapExchange");
+
+        Set<String> transactionCfgPropsDep = new HashSet<>();
+        transactionCfgPropsDep.add("txSerializableEnabled");
+        transactionCfgPropsDep.add("txManagerLookupClassName");
+        metadata.put(TransactionConfiguration.class,
+            new MetadataInfo(transactionCfgProps, transactionCfgPropsDep, EMPTY_FIELDS));
+
+        // Cache configuration.
+
+        Set<String> cacheCfgProps = new HashSet<>();
+        cacheCfgProps.add("name");
+        cacheCfgProps.add("groupName");
+        cacheCfgProps.add("cacheMode");
+        cacheCfgProps.add("atomicityMode");
+        cacheCfgProps.add("backups");
+        cacheCfgProps.add("partitionLossPolicy");
+        cacheCfgProps.add("readFromBackup");
+        cacheCfgProps.add("copyOnRead");
+        cacheCfgProps.add("invalidate");
+        cacheCfgProps.add("affinityMapper");
+        cacheCfgProps.add("topologyValidator");
+        cacheCfgProps.add("maxConcurrentAsyncOperations");
+        cacheCfgProps.add("defaultLockTimeout");
+        cacheCfgProps.add("writeSynchronizationMode");
+        cacheCfgProps.add("onheapCacheEnabled");
+        cacheCfgProps.add("dataRegionName");
+        // Removed since 2.0.
+        // cacheCfgProps.add("memoryMode");
+        // cacheCfgProps.add("offHeapMode");
+        // cacheCfgProps.add("offHeapMaxMemory");
+        cacheCfgProps.add("evictionPolicyFactory");
+        cacheCfgProps.add("evictionFilter");
+        // Removed since 2.0.
+        // cacheCfgProps.add("startSize");
+        // cacheCfgProps.add("swapEnabled");
+        cacheCfgProps.add("nearConfiguration");
+        cacheCfgProps.add("sqlSchema");
+        // Removed since 2.0.
+        // cacheCfgProps.add("sqlOnheapRowCacheSize");
+        cacheCfgProps.add("queryDetailMetricsSize");
+        cacheCfgProps.add("sqlFunctionClasses");
+        // Removed since 2.0
+        // cacheCfgProps.add("snapshotableIndex");
+        cacheCfgProps.add("sqlEscapeAll");
+        cacheCfgProps.add("queryParallelism");
+        cacheCfgProps.add("rebalanceMode");
+        cacheCfgProps.add("rebalanceBatchSize");
+        cacheCfgProps.add("rebalanceBatchesPrefetchCount");
+        cacheCfgProps.add("rebalanceOrder");
+        cacheCfgProps.add("rebalanceDelay");
+        cacheCfgProps.add("rebalanceTimeout");
+        cacheCfgProps.add("rebalanceThrottle");
+        cacheCfgProps.add("statisticsEnabled");
+        cacheCfgProps.add("managementEnabled");
+        cacheCfgProps.add("cacheStoreFactory");
+        cacheCfgProps.add("storeKeepBinary");
+        cacheCfgProps.add("loadPreviousValue");
+        cacheCfgProps.add("readThrough");
+        cacheCfgProps.add("writeThrough");
+        cacheCfgProps.add("writeBehindEnabled");
+        cacheCfgProps.add("writeBehindBatchSize");
+        cacheCfgProps.add("writeBehindFlushSize");
+        cacheCfgProps.add("writeBehindFlushFrequency");
+        cacheCfgProps.add("writeBehindFlushThreadCount");
+        cacheCfgProps.add("writeBehindCoalescing");
+        cacheCfgProps.add("indexedTypes");
+        cacheCfgProps.add("queryEntities");
+        cacheCfgProps.add("pluginConfigurations");
+        cacheCfgProps.add("cacheWriterFactory");
+        cacheCfgProps.add("cacheLoaderFactory");
+        cacheCfgProps.add("expiryPolicyFactory");
+        cacheCfgProps.add("storeConcurrentLoadAllThreshold");
+        cacheCfgProps.add("sqlIndexMaxInlineSize");
+        cacheCfgProps.add("sqlOnheapCacheEnabled");
+        cacheCfgProps.add("sqlOnheapCacheMaxSize");
+        cacheCfgProps.add("diskPageCompression");
+        cacheCfgProps.add("diskPageCompressionLevel");
+        cacheCfgProps.add("interceptor");
+        cacheCfgProps.add("storeByValue");
+        cacheCfgProps.add("eagerTtl");
+        cacheCfgProps.add("encryptionEnabled");
+        cacheCfgProps.add("eventsDisabled");
+        cacheCfgProps.add("maxQueryIteratorsCount");
+        cacheCfgProps.add("keyConfiguration");
+        cacheCfgProps.add("cacheStoreSessionListenerFactories");
+        cacheCfgProps.add("affinity");
+
+        Set<String> cacheCfgPropsDep = new HashSet<>();
+        // Removed since 2.0.
+        // cacheCfgPropsDep.add("atomicWriteOrderMode");
+        cacheCfgPropsDep.add("memoryPolicyName");
+        cacheCfgPropsDep.add("longQueryWarningTimeout");
+        cacheCfgPropsDep.add("rebalanceThreadPoolSize");
+        cacheCfgPropsDep.add("transactionManagerLookupClassName");
+        cacheCfgPropsDep.add("evictionPolicy");
+
+        Set<String> cacheCfgPropsExcl = new HashSet<>();
+        cacheCfgPropsExcl.add("nodeFilter");
+        cacheCfgPropsExcl.add("types");
+
+        metadata.put(CacheConfiguration.class, new MetadataInfo(cacheCfgProps, cacheCfgPropsDep, cacheCfgPropsExcl));
+
+        Set<String> rendezvousAffinityProps = new HashSet<>();
+        rendezvousAffinityProps.add("partitions");
+        rendezvousAffinityProps.add("affinityBackupFilter");
+        rendezvousAffinityProps.add("excludeNeighbors");
+
+        Set<String> rendezvousAffinityPropsDep = new HashSet<>();
+        rendezvousAffinityPropsDep.add("backupFilter");
+        metadata.put(RendezvousAffinityFunction.class,
+            new MetadataInfo(rendezvousAffinityProps, rendezvousAffinityPropsDep, EMPTY_FIELDS));
+
+        Set<String> nearCfgProps = new HashSet<>();
+        nearCfgProps.add("nearStartSize");
+        nearCfgProps.add("nearEvictionPolicyFactory");
+
+        Set<String> nearCfgPropsDep = new HashSet<>();
+        nearCfgPropsDep.add("nearEvictionPolicy");
+
+        metadata.put(NearCacheConfiguration.class, new MetadataInfo(nearCfgProps, nearCfgPropsDep, EMPTY_FIELDS));
+
+        Set<String> jdbcPojoStoreProps = new HashSet<>();
+        // Only setter for dataSource field.
+        // jdbcPojoStoreProps.add("dataSourceBean");
+        jdbcPojoStoreProps.add("dialect");
+        jdbcPojoStoreProps.add("batchSize");
+        jdbcPojoStoreProps.add("maximumPoolSize");
+        jdbcPojoStoreProps.add("maximumWriteAttempts");
+        jdbcPojoStoreProps.add("parallelLoadCacheMinimumThreshold");
+        jdbcPojoStoreProps.add("hasher");
+        jdbcPojoStoreProps.add("transformer");
+        jdbcPojoStoreProps.add("sqlEscapeAll");
+        jdbcPojoStoreProps.add("types");
+
+        // Configured via dataSource property.
+        Set<String> jdbcPojoStorePropsExcl = new HashSet<>();
+        jdbcPojoStorePropsExcl.add("dataSourceBean");
+        jdbcPojoStorePropsExcl.add("dataSourceFactory");
+
+        metadata.put(CacheJdbcPojoStoreFactory.class,
+            new MetadataInfo(jdbcPojoStoreProps, EMPTY_FIELDS, jdbcPojoStorePropsExcl));
+
+        Set<String> jdbcBlobStoreProps = new HashSet<>();
+        jdbcBlobStoreProps.add("connectionUrl");
+        jdbcBlobStoreProps.add("user");
+        // Only setter for dataSource.
+        // jdbcBlobStoreProps.add("dataSourceBean");
+        // jdbcBlobStoreProps.add("dialect");
+        jdbcBlobStoreProps.add("initSchema");
+        jdbcBlobStoreProps.add("createTableQuery");
+        jdbcBlobStoreProps.add("loadQuery");
+        jdbcBlobStoreProps.add("insertQuery");
+        jdbcBlobStoreProps.add("updateQuery");
+        jdbcBlobStoreProps.add("deleteQuery");
+        metadata.put(CacheJdbcBlobStore.class, new MetadataInfo(jdbcBlobStoreProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> hibernateBlobStoreProps = new HashSet<>();
+        hibernateBlobStoreProps.add("hibernateProperties");
+        metadata.put(CacheHibernateBlobStore.class,
+            new MetadataInfo(hibernateBlobStoreProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> igfsCfgProps = new HashSet<>();
+        igfsCfgProps.add("name");
+        igfsCfgProps.add("defaultMode");
+        // Removed since 2.0.
+        // igfsCfgProps.add("dualModeMaxPendingPutsSize");
+        // igfsCfgProps.add("dualModePutExecutorService");
+        // igfsCfgProps.add("dualModePutExecutorServiceShutdown");
+        igfsCfgProps.add("fragmentizerEnabled");
+        igfsCfgProps.add("fragmentizerConcurrentFiles");
+        igfsCfgProps.add("fragmentizerThrottlingBlockLength");
+        igfsCfgProps.add("fragmentizerThrottlingDelay");
+        igfsCfgProps.add("ipcEndpointEnabled");
+        igfsCfgProps.add("ipcEndpointConfiguration");
+        igfsCfgProps.add("blockSize");
+        // streamBufferSize field in model.
+        igfsCfgProps.add("bufferSize");
+        // Removed since 2.0.
+        // igfsCfgProps.add("streamBufferSize");
+        // igfsCfgProps.add("maxSpaceSize");
+        igfsCfgProps.add("maximumTaskRangeLength");
+        igfsCfgProps.add("managementPort");
+        igfsCfgProps.add("perNodeBatchSize");
+        igfsCfgProps.add("perNodeParallelBatchCount");
+        igfsCfgProps.add("prefetchBlocks");
+        igfsCfgProps.add("sequentialReadsBeforePrefetch");
+        // Removed since 2.0.
+        // igfsCfgProps.add("trashPurgeTimeout");
+        igfsCfgProps.add("colocateMetadata");
+        igfsCfgProps.add("relaxedConsistency");
+        igfsCfgProps.add("updateFileLengthOnFlush");
+        igfsCfgProps.add("pathModes");
+        igfsCfgProps.add("secondaryFileSystem");
+
+        Set<String> igfsCfgPropsExclude = new HashSet<>();
+        igfsCfgPropsExclude.add("dataCacheConfiguration");
+        igfsCfgPropsExclude.add("metaCacheConfiguration");
+
+        metadata.put(FileSystemConfiguration.class, new MetadataInfo(igfsCfgProps, EMPTY_FIELDS, igfsCfgPropsExclude));
+
+        Set<String> igfsBlocMapperProps = new HashSet<>();
+        igfsBlocMapperProps.add("groupSize");
+
+        metadata.put(IgfsGroupDataBlocksKeyMapper.class,
+            new MetadataInfo(igfsBlocMapperProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> secHadoopIgfsCfgProps = new HashSet<>();
+        secHadoopIgfsCfgProps.add("defaultUserName");
+        secHadoopIgfsCfgProps.add("fileSystemFactory");
+
+        metadata.put(IgniteHadoopIgfsSecondaryFileSystem.class, new MetadataInfo(secHadoopIgfsCfgProps, EMPTY_FIELDS,
+            EMPTY_FIELDS));
+
+        Set<String> cachingIgfsCfgProps = new HashSet<>();
+        cachingIgfsCfgProps.add("uri");
+        cachingIgfsCfgProps.add("configPaths");
+        cachingIgfsCfgProps.add("userNameMapper");
+
+        metadata.put(CachingHadoopFileSystemFactory.class,
+            new MetadataInfo(cachingIgfsCfgProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> kerberosIgfsCfgProps = new HashSet<>();
+        kerberosIgfsCfgProps.add("uri");
+        kerberosIgfsCfgProps.add("configPaths");
+        kerberosIgfsCfgProps.add("userNameMapper");
+        kerberosIgfsCfgProps.add("keyTab");
+        kerberosIgfsCfgProps.add("keyTabPrincipal");
+        kerberosIgfsCfgProps.add("reloginInterval");
+
+        metadata.put(KerberosHadoopFileSystemFactory.class, new MetadataInfo(kerberosIgfsCfgProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> chainedIgfsUsrNameMapperProps = new HashSet<>();
+        chainedIgfsUsrNameMapperProps.add("mappers");
+
+        metadata.put(ChainedUserNameMapper.class, new MetadataInfo(chainedIgfsUsrNameMapperProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> basicIgfsUsrNameMapperProps = new HashSet<>();
+        basicIgfsUsrNameMapperProps.add("defaultUserName");
+        basicIgfsUsrNameMapperProps.add("useDefaultUserName");
+        basicIgfsUsrNameMapperProps.add("mappings");
+
+        metadata.put(BasicUserNameMapper.class, new MetadataInfo(basicIgfsUsrNameMapperProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> kerberosIgfsUsrNameMapperProps = new HashSet<>();
+        kerberosIgfsUsrNameMapperProps.add("instance");
+        kerberosIgfsUsrNameMapperProps.add("realm");
+
+        metadata.put(KerberosUserNameMapper.class, new MetadataInfo(kerberosIgfsUsrNameMapperProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> ipcEndpointProps = new HashSet<>();
+        ipcEndpointProps.add("type");
+        ipcEndpointProps.add("host");
+        ipcEndpointProps.add("port");
+        ipcEndpointProps.add("memorySize");
+        ipcEndpointProps.add("threadCount");
+        ipcEndpointProps.add("tokenDirectoryPath");
+        metadata.put(IgfsIpcEndpointConfiguration.class, new MetadataInfo(ipcEndpointProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> qryEntityProps = new HashSet<>();
+        qryEntityProps.add("keyType");
+        qryEntityProps.add("valueType");
+        qryEntityProps.add("aliases");
+        qryEntityProps.add("fields");
+        qryEntityProps.add("indexes");
+        qryEntityProps.add("tableName");
+        qryEntityProps.add("keyFieldName");
+        qryEntityProps.add("valueFieldName");
+        qryEntityProps.add("keyFields");
+        qryEntityProps.add("fieldsPrecision");
+        qryEntityProps.add("notNullFields");
+        qryEntityProps.add("fieldsScale");
+        qryEntityProps.add("defaultFieldValues");
+        metadata.put(QueryEntity.class, new MetadataInfo(qryEntityProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> qryIdxProps = new HashSet<>();
+        qryIdxProps.add("name");
+        qryIdxProps.add("indexType");
+        qryIdxProps.add("fields");
+        qryIdxProps.add("inlineSize");
+
+        Set<String> qryIdxPropsExcl = new HashSet<>();
+        qryIdxPropsExcl.add("fieldNames");
+
+        metadata.put(QueryIndex.class, new MetadataInfo(qryIdxProps, EMPTY_FIELDS, qryIdxPropsExcl));
+
+        Set<String> jdbcTypeProps = new HashSet<>();
+        jdbcTypeProps.add("cacheName");
+        jdbcTypeProps.add("keyType");
+        jdbcTypeProps.add("valueType");
+        jdbcTypeProps.add("databaseSchema");
+        jdbcTypeProps.add("databaseTable");
+        jdbcTypeProps.add("keyFields");
+        jdbcTypeProps.add("valueFields");
+
+        metadata.put(JdbcType.class, new MetadataInfo(jdbcTypeProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> sorterEvictionProps = new HashSet<>();
+        sorterEvictionProps.add("batchSize");
+        sorterEvictionProps.add("maxMemorySize");
+        sorterEvictionProps.add("maxSize");
+        metadata.put(SortedEvictionPolicy.class, new MetadataInfo(sorterEvictionProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> lruEvictionProps = new HashSet<>();
+        lruEvictionProps.add("batchSize");
+        lruEvictionProps.add("maxMemorySize");
+        lruEvictionProps.add("maxSize");
+        metadata.put(LruEvictionPolicy.class, new MetadataInfo(lruEvictionProps, EMPTY_FIELDS, EMPTY_FIELDS));
+
+        Set<String> fifoEvictionProps = new HashSet<>();
+        fifoEvictionProps.add("batchSize");
+        fifoEvictionProps.add("maxMemorySize");
+        fifoEvictionProps.add("maxSize");
+        metadata.put(FifoEvictionPolicy.class, new MetadataInfo(fifoEvictionProps, EMPTY_FIELDS, EMPTY_FIELDS));
+    }
+
+    /**
+     * Check an accordance of possible to configure properties and configuration classes.
+     */
+    @Test
+    public void testConfiguration() {
+        prepareMetadata();
+
+        HashMap<Class<?>, WrongFields> diff = new HashMap<>();
+
+        for (Map.Entry<Class<?>, MetadataInfo> ent: metadata.entrySet()) {
+            Class<?> cls = ent.getKey();
+            MetadataInfo meta = ent.getValue();
+
+            Set<String> props = meta.getGeneratedFields();
+            Set<String> knownDeprecated = meta.getDeprecatedFields();
+            Set<String> excludeFields = meta.getExcludedFields();
+
+            boolean clsDeprecated = cls.getAnnotation(Deprecated.class) != null;
+            Map<String, FieldProcessingInfo> clsProps = new HashMap<>();
+
+            for (Method m: cls.getMethods()) {
+                String mtdName = m.getName();
+
+                String propName = mtdName.length() > 3 && (mtdName.startsWith("get") || mtdName.startsWith("set")) ?
+                    mtdName.toLowerCase().charAt(3) + mtdName.substring(4) :
+                    mtdName.length() > 2 && mtdName.startsWith("is") ?
+                        mtdName.toLowerCase().charAt(2) + mtdName.substring(3) : null;
+
+                boolean deprecated = clsDeprecated || m.getAnnotation(Deprecated.class) != null;
+
+                if (propName != null && !excludeFields.contains(propName)) {
+                    clsProps.put(propName,
+                        clsProps
+                            .getOrDefault(propName, new FieldProcessingInfo(propName, 0, deprecated))
+                            .deprecated(deprecated)
+                            .next());
+                }
+            }
+
+            Set<String> missedFields = new HashSet<>();
+            Set<String> deprecatedFields = new HashSet<>();
+
+            for (Map.Entry<String, FieldProcessingInfo> e: clsProps.entrySet()) {
+                String prop = e.getKey();
+                FieldProcessingInfo info = e.getValue();
+
+                if (info.getOccurrence() > 1 && !info.isDeprecated() && !props.contains(prop))
+                    missedFields.add(prop);
+
+                if (info.getOccurrence() > 1 && info.isDeprecated() && !props.contains(prop) && !knownDeprecated.contains(prop))
+                    deprecatedFields.add(prop);
+            }
+
+            Set<String> rmvFields = new HashSet<>();
+
+            for (String p: props)
+                if (!clsProps.containsKey(p))
+                    rmvFields.add(p);
+
+            for (String p: knownDeprecated)
+                if (!clsProps.containsKey(p))
+                    rmvFields.add(p);
+
+            WrongFields fields = new WrongFields(missedFields, deprecatedFields, rmvFields);
+
+            if (fields.nonEmpty()) {
+                diff.put(cls, fields);
+
+                log("Result for class: " + cls.getName());
+
+                if (!missedFields.isEmpty()) {
+                    log("  Missed");
+
+                    for (String fld: missedFields)
+                        log("    " + fld);
+                }
+
+                if (!deprecatedFields.isEmpty()) {
+                    log("  Deprecated");
+
+                    for (String fld: deprecatedFields)
+                        log("    " + fld);
+                }
+
+                if (!rmvFields.isEmpty()) {
+                    log("  Removed");
+
+                    for (String fld: rmvFields)
+                        log("    " + fld);
+                }
+
+                log("");
+            }
+        }
+
+        // Test will pass only if no difference found between IgniteConfiguration and Web Console generated configuration.
+        assert diff.isEmpty() : "Found difference between IgniteConfiguration and Web Console";
+    }
+}
diff --git a/modules/compatibility/src/test/java/org/apache/ignite/console/configuration/WrongFields.java b/modules/compatibility/src/test/java/org/apache/ignite/console/configuration/WrongFields.java
new file mode 100644
index 0000000..1efe394
--- /dev/null
+++ b/modules/compatibility/src/test/java/org/apache/ignite/console/configuration/WrongFields.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.configuration;
+
+import java.util.Set;
+
+/**
+ * Service class with information about class fields, which have problems in configurator.
+ */
+@SuppressWarnings("AssignmentOrReturnOfFieldWithMutableType")
+public class WrongFields {
+    /** Missing in configuration fields. */
+    private final Set<String> fields;
+
+    /** Deprecated in configuration classes fields. */
+    private final Set<String> deprecatedFields;
+
+    /** Removed in configuration classes fields. */
+    private final Set<String> rmvFields;
+
+    /**
+     * @param fields Missing fields.
+     * @param deprecatedFields Deprecated fields.
+     * @param rmvFields Removed fields.
+     */
+    public WrongFields(Set<String> fields, Set<String> deprecatedFields, Set<String> rmvFields) {
+        this.fields = fields;
+        this.deprecatedFields = deprecatedFields;
+        this.rmvFields = rmvFields;
+    }
+
+    /**
+     * @return Missed at configurator fields.
+     */
+    public Set<String> getFields() {
+        return fields;
+    }
+
+    /**
+     * @return Deprecated in configuration classes fields.
+     */
+    public Set<String> getDeprecatedFields() {
+        return deprecatedFields;
+    }
+
+    /**
+     * @return Removed in configuration classes fields.
+     */
+    public Set<String> getRemovedFields() {
+        return rmvFields;
+    }
+
+    /**
+     * Check that wrong fields are exists.
+     *
+     * @return {@code true} when problems in configurator are exist or {@code false} otherwise.
+     */
+    public boolean nonEmpty() {
+        return !fields.isEmpty() || !deprecatedFields.isEmpty() || !rmvFields.isEmpty();
+    }
+}
diff --git a/modules/compatibility/src/test/java/org/apache/ignite/console/testsuites/WebConsoleTestSuite.java b/modules/compatibility/src/test/java/org/apache/ignite/console/testsuites/WebConsoleTestSuite.java
new file mode 100644
index 0000000..249d5fb
--- /dev/null
+++ b/modules/compatibility/src/test/java/org/apache/ignite/console/testsuites/WebConsoleTestSuite.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.testsuites;
+
+import org.apache.ignite.console.configuration.WebConsoleConfigurationSelfTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * Ignite Web Console test suite.
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    WebConsoleConfigurationSelfTest.class,
+})
+public class WebConsoleTestSuite {
+}
diff --git a/modules/e2e/docker-compose.yml b/modules/e2e/docker-compose.yml
new file mode 100644
index 0000000..77c4487
--- /dev/null
+++ b/modules/e2e/docker-compose.yml
@@ -0,0 +1,42 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+version: '3'
+services:
+  mongodb:
+    image: mongo:4.0
+    container_name: 'mongodb'
+
+  testenv:
+    build:
+      context: '../'
+      dockerfile: './e2e/testenv/Dockerfile'
+    environment:
+      - mongodb_url=mongodb://mongodb:27017/console-e2e
+    depends_on:
+      - mongodb
+
+  e2e:
+    build: './testcafe'
+    environment:
+      - DB_URL=mongodb://mongodb:27017/console-e2e
+      - APP_URL=http://testenv:9001/
+      - REPORTER=teamcity
+      - QUARANTINE_MODE=true
+    depends_on:
+      - mongodb
+      - testenv
diff --git a/modules/e2e/testcafe/.eslintrc b/modules/e2e/testcafe/.eslintrc
new file mode 100644
index 0000000..43540b8
--- /dev/null
+++ b/modules/e2e/testcafe/.eslintrc
@@ -0,0 +1 @@
+extends: "./../../frontend/.eslintrc"
\ No newline at end of file
diff --git a/modules/e2e/testcafe/Dockerfile b/modules/e2e/testcafe/Dockerfile
new file mode 100644
index 0000000..d207eec
--- /dev/null
+++ b/modules/e2e/testcafe/Dockerfile
@@ -0,0 +1,32 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+FROM testcafe/testcafe:latest
+
+USER 0
+
+WORKDIR /opt/testcafe/tests
+
+COPY . .
+
+ENV NPM_CONFIG_LOGLEVEL error
+
+RUN npm install --no-optional --production && \
+ npm cache verify --force && \
+ rm -rf /tmp/*
+
+ENTRYPOINT ["node", "./index.js"]
\ No newline at end of file
diff --git a/modules/e2e/testcafe/components/FormField.js b/modules/e2e/testcafe/components/FormField.js
new file mode 100644
index 0000000..0c6c4eb
--- /dev/null
+++ b/modules/e2e/testcafe/components/FormField.js
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector, t} from 'testcafe';
+import {AngularJSSelector} from 'testcafe-angular-selectors';
+
+export class FormField {
+    static ROOT_SELECTOR = '.form-field';
+    static LABEL_SELECTOR = '.form-field__label';
+    static CONTROL_SELECTOR = '[ng-model]';
+    static ERRORS_SELECTOR = '.form-field__errors';
+
+    /** @type {ReturnType<Selector>} */
+    _selector;
+
+    constructor({id = '', label = '', model = ''} = {}) {
+        if (!id && !label && !model) throw new Error('ID, label or model are required');
+        if (id)
+            this._selector = Selector(`#${id}`).parent(this.constructor.ROOT_SELECTOR);
+        else if (label) {
+            this._selector = Selector((LABEL_SELECTOR, ROOT_SELECTOR, label) => {
+                return [].slice.call((window.document.querySelectorAll(LABEL_SELECTOR)))
+                    .filter((el) => el.textContent.includes(label))
+                    .map((el) => el.closest(ROOT_SELECTOR))
+                    .pop();
+            })(this.constructor.LABEL_SELECTOR, this.constructor.ROOT_SELECTOR, label);
+        } else if (model)
+            this._selector = AngularJSSelector.byModel(model).parent(this.constructor.ROOT_SELECTOR);
+
+        this.label = this._selector.find(this.constructor.LABEL_SELECTOR);
+        this.control = this._selector.find(this.constructor.CONTROL_SELECTOR);
+        this.errors = this._selector.find(this.constructor.ERRORS_SELECTOR);
+    }
+    /**
+     * Selects dropdown option
+     * @param {string} label
+     */
+    async selectOption(label) {
+        await t
+            .click(this.control)
+            .click(Selector('.bssm-item-button').withText(label));
+    }
+    /**
+     * Get error element by error type
+     * @param {string} errorType
+     */
+    getError(errorType) {
+        // return this._selector.find(`.form-field__error`)
+        return this._selector.find(`[ng-message="${errorType}"]`);
+    }
+    get selectedOption() {
+        return this.control.textContent;
+    }
+    get postfix() {
+        return this._selector.find('[data-postfix]').getAttribute('data-postfix');
+    }
+}
+
+/**
+ * Not really a custom field, use for form fields at login and profile screens, these don't have "ignite" prefix
+ */
+export class CustomFormField extends FormField {
+    static ROOT_SELECTOR = '.form-field';
+    static LABEL_SELECTOR = '.form-field__label';
+    static ERRORS_SELECTOR = '.form-field__errors';
+    constructor(...args) {
+        super(...args);
+        this.errors = this.errors.addCustomMethods({
+            hasError(errors, errorMessage) {
+                return !!errors.querySelectorAll(`.form-field__error [data-title*="${errorMessage}"]`).length;
+            }
+        });
+    }
+}
diff --git a/modules/e2e/testcafe/components/ListEditable.js b/modules/e2e/testcafe/components/ListEditable.js
new file mode 100644
index 0000000..acce0c6
--- /dev/null
+++ b/modules/e2e/testcafe/components/ListEditable.js
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector, t} from 'testcafe'
+import {FormField} from './FormField'
+
+const addItemButton = Selector(value => {
+    value = value();
+    const innerButton = value.querySelector('.le-row:not(.ng-hide) list-editable-add-item-button [ng-click]');
+
+    if (innerButton)
+        return innerButton;
+
+    /** @type {Element} */
+    const outerButton = value.nextElementSibling;
+
+    if (outerButton.getAttribute('ng-click') === '$ctrl.addItem()')
+        return outerButton;
+});
+
+export class ListEditableItem {
+    /**
+     * @param {Selector} selector
+     * @param {Object.<string, {id: string}>} fieldsMap
+     */
+    constructor(selector, fieldsMap = {}) {
+        this._selector = selector;
+        this._fieldsMap = fieldsMap;
+        /** @type {SelectorAPI} */
+        this.editView = this._selector.find('list-editable-item-edit');
+        /** @type {SelectorAPI} */
+        this.itemView = this._selector.find('list-editable-item-view');
+        /** @type {Object.<string, FormField>} Inline form fields */
+        this.fields = Object.keys(fieldsMap).reduce((acc, key) => ({...acc, [key]: new FormField(this._fieldsMap[key])}), {})
+    }
+    async startEdit() {
+        await t.click(this.itemView)
+    }
+    async stopEdit() {
+        await t.click('.wrapper')
+    }
+    /**
+     * @param {number} index
+     */
+    getItemViewColumn(index) {
+        return this.itemView.child(index)
+    }
+}
+
+export class ListEditable {
+    static ADD_ITEM_BUTTON_SELECTOR = '[ng-click="$ctrl.addItem()"]';
+    /** @param {SelectorAPI} selector */
+    constructor(selector, fieldsMap) {
+        this._selector = selector;
+        this._fieldsMap = fieldsMap;
+        this.addItemButton = Selector(addItemButton(selector))
+    }
+
+    async addItem() {
+        await t.click(this.addItemButton)
+    }
+
+    /**
+     * @param {number} index Zero-based index of item in the list
+     */
+    getItem(index) {
+        return new ListEditableItem(this._selector.find(`.le-body>.le-row[ng-repeat]`).nth(index), this._fieldsMap)
+    }
+}
diff --git a/modules/e2e/testcafe/components/PanelCollapsible.js b/modules/e2e/testcafe/components/PanelCollapsible.js
new file mode 100644
index 0000000..d58d48b
--- /dev/null
+++ b/modules/e2e/testcafe/components/PanelCollapsible.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector} from 'testcafe';
+
+export class PanelCollapsible {
+    constructor(title) {
+        this._selector = Selector('.panel-collapsible__title').withText(title).parent('panel-collapsible');
+        this.heading = this._selector.find('.panel-collapsible__heading');
+        this.body = this._selector.find('.panel-collapsible__content').addCustomDOMProperties({
+            isOpened: (el) => !el.classList.contains('ng-hide')
+        });
+    }
+}
diff --git a/modules/e2e/testcafe/components/Table.js b/modules/e2e/testcafe/components/Table.js
new file mode 100644
index 0000000..415f98d
--- /dev/null
+++ b/modules/e2e/testcafe/components/Table.js
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector, t} from 'testcafe';
+
+const findCell = Selector((table, rowIndex, columnLabel) => {
+    table = table();
+
+    const columnIndex = [].constructor.from(
+        table.querySelectorAll('.ui-grid-header-cell:not(.ui-grid-header-span)'),
+        (e) => e.textContent
+    ).findIndex((t) => t.includes(columnLabel));
+
+    const row = table.querySelector(`.ui-grid-render-container:not(.left) .ui-grid-viewport .ui-grid-row:nth-of-type(${rowIndex + 1})`);
+    const cell = row.querySelector(`.ui-grid-cell:nth-of-type(${columnIndex})`);
+
+    return cell;
+});
+
+export class Table {
+    /** @param {ReturnType<Selector>} selector */
+    constructor(selector) {
+        this._selector = selector;
+        this.title = this._selector.find('.panel-title');
+        this.actionsButton = this._selector.find('.btn-ignite').withText('Actions');
+        this.allItemsCheckbox = this._selector.find('[role="checkbox button"]');
+    }
+
+    /** @param {string} label */
+    async performAction(label) {
+        await t.hover(this.actionsButton).click(Selector('.dropdown-menu a').withText(label));
+    }
+
+    /**
+     * Toggles grid row selection
+     * @param {number} index Index of row, starting with 1
+     */
+    async toggleRowSelection(index) {
+        await t.click(this._selector.find(`.ui-grid-pinned-container .ui-grid-row:nth-of-type(${index}) .ui-grid-selection-row-header-buttons`));
+    }
+
+    /**
+     * @param {number} rowIndex
+     * @param {string} columnLabel
+     */
+    findCell(rowIndex, columnLabel) {
+        return Selector(findCell(this._selector, rowIndex, columnLabel));
+    }
+}
diff --git a/modules/e2e/testcafe/components/confirmation.js b/modules/e2e/testcafe/components/confirmation.js
new file mode 100644
index 0000000..cdfdd54
--- /dev/null
+++ b/modules/e2e/testcafe/components/confirmation.js
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector, t} from 'testcafe';
+
+const body = Selector('.modal').withText('Confirmation').find('.modal-body');
+const confirmButton = Selector('#confirm-btn-ok');
+const cancelButton = Selector('#confirm-btn-cancel');
+const closeButton = Selector('.modal').withText('Confirmation').find('.modal .close');
+
+export const confirmation = {
+    body,
+    confirmButton,
+    cancelButton,
+    closeButton,
+    async confirm() {
+        await t.click(confirmButton);
+    },
+    async cancel() {
+        await t.click(cancelButton);
+    },
+    async close() {
+        await t.click(closeButton);
+    }
+};
diff --git a/modules/e2e/testcafe/components/modalInput.js b/modules/e2e/testcafe/components/modalInput.js
new file mode 100644
index 0000000..845a3d7
--- /dev/null
+++ b/modules/e2e/testcafe/components/modalInput.js
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {FormField} from './FormField';
+import {t} from 'testcafe';
+
+export class ModalInput {
+    constructor() {
+        this.valueInput = new FormField({ id: 'inputDialogFieldInput' });
+    }
+
+    async enterValue(value) {
+        await t.typeText(this.valueInput.control, value);
+    }
+
+    async confirm() {
+        await t.click('#copy-btn-confirm');
+    }
+
+    async cancel() {
+        await t.click('#copy-btn-cancel');
+    }
+
+    async close() {
+        await t.click('.modal .close');
+    }
+}
diff --git a/modules/e2e/testcafe/components/no-data.js b/modules/e2e/testcafe/components/no-data.js
new file mode 100644
index 0000000..12be3a6
--- /dev/null
+++ b/modules/e2e/testcafe/components/no-data.js
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector} from 'testcafe';
+
+export const noDataCmp = Selector('no-data');
diff --git a/modules/e2e/testcafe/components/notifications.js b/modules/e2e/testcafe/components/notifications.js
new file mode 100644
index 0000000..f1bfa8f
--- /dev/null
+++ b/modules/e2e/testcafe/components/notifications.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector} from 'testcafe';
+
+export const successNotification = Selector('body > .alert-success');
+export const errorNotification = Selector('body > .alert-danger');
diff --git a/modules/e2e/testcafe/components/pageAdvancedConfiguration.js b/modules/e2e/testcafe/components/pageAdvancedConfiguration.js
new file mode 100644
index 0000000..627a345
--- /dev/null
+++ b/modules/e2e/testcafe/components/pageAdvancedConfiguration.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector, t} from 'testcafe';
+
+export const pageAdvancedConfiguration = {
+    saveButton: Selector('.pc-form-actions-panel .btn-ignite').withText('Save'),
+    clusterNavButton: Selector('.pca-menu-link[ui-sref="base.configuration.edit.advanced.cluster"]'),
+    modelsNavButton: Selector('.pca-menu-link[ui-sref="base.configuration.edit.advanced.models"]'),
+    cachesNavButton: Selector('.pca-menu-link[ui-sref="base.configuration.edit.advanced.caches"]'),
+    igfsNavButton: Selector('.pca-menu-link[ui-sref="base.configuration.edit.advanced.igfs"]'),
+    async save() {
+        await t.click(this.saveButton);
+    }
+};
diff --git a/modules/e2e/testcafe/components/pageConfiguration.js b/modules/e2e/testcafe/components/pageConfiguration.js
new file mode 100644
index 0000000..033412b
--- /dev/null
+++ b/modules/e2e/testcafe/components/pageConfiguration.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector} from 'testcafe';
+
+export const basicNavButton = Selector('.tabs.tabs--blue a[ui-sref="base.configuration.edit.basic"]');
+export const advancedNavButton = Selector('.tabs.tabs--blue a[ui-sref="base.configuration.edit.advanced.cluster"]');
diff --git a/modules/e2e/testcafe/components/topNavigation.js b/modules/e2e/testcafe/components/topNavigation.js
new file mode 100644
index 0000000..6db745e
--- /dev/null
+++ b/modules/e2e/testcafe/components/topNavigation.js
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector} from 'testcafe';
+
+export const toggleMenuButton = Selector('.web-console-header__togle-menu-button');
+
+export const configureNavButton = Selector('.web-console-sidebar-navigation__link[title="Configuration"]');
+export const queriesNavButton = Selector('.web-console-sidebar-navigation__link[title="Queries"]');
diff --git a/modules/e2e/testcafe/components/userMenu.js b/modules/e2e/testcafe/components/userMenu.js
new file mode 100644
index 0000000..973f21f
--- /dev/null
+++ b/modules/e2e/testcafe/components/userMenu.js
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector, t} from 'testcafe';
+
+const _selector = Selector('user-menu');
+
+export const userMenu = {
+    _selector,
+    button: _selector.find('[bs-dropdown]'),
+    /** 
+     * Clicks on user menu option.
+     * @param {string} label Menu option label to click on
+     */
+    async clickOption(label) {
+        return t
+            .hover(this.button)
+            .click(_selector.find(`[role="menuitem"]`).withText(label));
+    }
+};
diff --git a/modules/e2e/testcafe/environment/envtools.js b/modules/e2e/testcafe/environment/envtools.js
new file mode 100644
index 0000000..0952951
--- /dev/null
+++ b/modules/e2e/testcafe/environment/envtools.js
@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const MongoClient = require('mongodb').MongoClient;
+const objectid = require('objectid');
+const { spawn } = require('child_process');
+const url = require('url');
+
+const argv = require('minimist')(process.argv.slice(2));
+const start = argv._.includes('start');
+const stop = argv._.includes('stop');
+
+const mongoUrl = process.env.DB_URL || 'mongodb://localhost/console-e2e';
+
+const insertTestUser = ({userId = '000000000000000000000001', token = 'ppw4tPI3JUOGHva8CODO'} = {}) => {
+    return new Promise((res, rej) => {
+        MongoClient
+            .connect(mongoUrl, function(err, db) {
+                if (err) {
+                    rej();
+                    throw err;
+                }
+
+                // Add test user.
+                const user = {
+                    _id: objectid(userId),
+                    salt: 'ca8b49c2eacd498a0973de30c0873c166ed99fa0605981726aedcc85bee17832',
+                    hash: 'c052c87e454cd0875332719e1ce085ccd92bedb73c8f939ba45d387f724da97128280643ad4f841d929d48de802f48f4a27b909d2dc806d957d38a1a4049468ce817490038f00ac1416aaf9f8f5a5c476730b46ea22d678421cd269869d4ba9d194f73906e5d5a4fec5229459e20ebda997fb95298067126f6c15346d886d44b67def03bf3ffe484b2e4fa449985de33a0c12e4e1da4c7d71fe7af5d138433f703d8c7eeebbb3d57f1a89659010a1f1d3cd4fbc524abab07860daabb08f08a28b8bfc64ecde2ea3c103030d0d54fc24d9c02f92ee6b3aa1bcd5c70113ab9a8045faea7dd2dc59ec4f9f69fcf634232721e9fb44012f0e8c8fdf7c6bf642db6867ef8e7877123e1bc78af7604fee2e34ad0191f8b97613ea458e0fca024226b7055e08a4bdb256fabf0a203a1e5b6a6c298fb0c60308569cefba779ce1e41fb971e5d1745959caf524ab0bedafce67157922f9c505cea033f6ed28204791470d9d08d31ce7e8003df8a3a05282d4d60bfe6e2f7de06f4b18377dac0fe764ed683c9b2553e75f8280c748aa166fef6f89190b1c6d369ab86422032171e6f9686de42ac65708e63bf018a043601d85bc5c820c7ad1d51ded32e59cdaa629a3f7ae325bbc931f9f21d90c9204effdbd53721a60c8b180dd8c236133e287a47ccc9e5072eb6593771e435e4d5196d50d6ddb32c226651c6503387895c5ad025f69fd3',
+                    password: 'a',
+                    email: 'a@a',
+                    firstName: 'John',
+                    lastName: 'Doe',
+                    company: 'TestCompany',
+                    country: 'Canada',
+                    industry: 'Banking',
+                    admin: true,
+                    token,
+                    attempts: 0,
+                    lastLogin: '2018-01-28T10:41:07.463Z',
+                    resetPasswordToken: '892rnLbEnVp1FP75Jgpi'
+                };
+                db.collection('accounts').insert(user);
+
+                // Add test spaces.
+
+                const spaces = [
+                    {
+                        _id: objectid('000000000000000000000001'),
+                        name: 'Personal space',
+                        owner: objectid(userId),
+                        demo: false
+                    },
+                    {
+                        _id: objectid('000000000000000000000002'),
+                        name: 'Demo space',
+                        owner: objectid(userId),
+                        demo: true
+                    }
+                ];
+                db.collection('spaces').insertMany(spaces);
+
+                db.close();
+                res();
+
+            });
+    });
+};
+
+const dropTestDB = () => {
+    return new Promise((resolve, reject) => {
+        MongoClient.connect(mongoUrl, async(err, db) => {
+            if (err)
+                return reject(err);
+
+            db.dropDatabase((err) => {
+                if (err)
+                    return reject(err);
+
+                resolve();
+            });
+        });
+    });
+};
+
+
+/**
+ * Spawns a new process using the given command.
+ * @param command {String} The command to run.
+ * @param onResolveString {String} Await string in output.
+ * @param cwd {String} Current working directory of the child process.
+ * @param env {Object} Environment key-value pairs.
+ * @return {Promise<ChildProcess>}
+ */
+const exec = (command, onResolveString, cwd, env) => {
+    return new Promise((resolve) => {
+        env = Object.assign({}, process.env, env, { FORCE_COLOR: true });
+
+        const [cmd, ...args] = command.split(' ');
+
+        const detached = process.platform !== 'win32';
+
+        const child = spawn(cmd, args, {cwd, env, detached});
+
+        if (detached) {
+            // do something when app is closing
+            process.on('exit', () => process.kill(-child.pid));
+
+            // catches ctrl+c event
+            process.on('SIGINT', () => process.kill(-child.pid));
+
+            // catches "kill pid" (for example: nodemon restart)
+            process.on('SIGUSR1', () => process.kill(-child.pid));
+            process.on('SIGUSR2', () => process.kill(-child.pid));
+
+            // catches uncaught exceptions
+            process.on('uncaughtException', () => process.kill(-child.pid));
+        }
+
+        // Pipe error messages to stdout.
+        child.stderr.on('data', (data) => {
+            process.stdout.write(data.toString());
+        });
+
+        child.stdout.on('data', (data) => {
+            process.stdout.write(data.toString());
+
+            if (data.includes(onResolveString))
+                resolve(child);
+        });
+    });
+};
+
+const startEnv = (webConsoleRootDirectoryPath = '../../') => {
+    return new Promise(async(resolve) => {
+        const BACKEND_PORT = 3001;
+        const command = `${process.platform === 'win32' ? 'npm.cmd' : 'npm'} start`;
+
+        let port = 9001;
+
+        if (process.env.APP_URL)
+            port = parseInt(url.parse(process.env.APP_URL).port, 10) || 80;
+
+        const backendInstanceLaunch = exec(command, 'Start listening', `${webConsoleRootDirectoryPath}backend`, {server_port: BACKEND_PORT, mongodb_url: mongoUrl});
+        const frontendInstanceLaunch = exec(command, 'Compiled successfully', `${webConsoleRootDirectoryPath}frontend`, {BACKEND_URL: `http://localhost:${BACKEND_PORT}`, PORT: port});
+
+        console.log('Building backend in progress...');
+        await backendInstanceLaunch;
+        console.log('Building backend done!');
+
+        console.log('Building frontend in progress...');
+        await frontendInstanceLaunch;
+        console.log('Building frontend done!');
+
+        resolve();
+    });
+};
+
+if (start) {
+    startEnv();
+
+    process.on('SIGINT', async() => {
+        await dropTestDB();
+
+        process.exit(0);
+    });
+}
+
+if (stop) {
+    dropTestDB();
+
+    console.log('Cleaning done...');
+}
+
+
+/**
+ * @param {string} targetUrl
+ * @returns {string}
+ */
+const resolveUrl = (targetUrl) => {
+    return url.resolve(process.env.APP_URL || 'http://localhost:9001', targetUrl);
+};
+
+module.exports = { startEnv, insertTestUser, dropTestDB, resolveUrl };
diff --git a/modules/e2e/testcafe/environment/launch-env.js b/modules/e2e/testcafe/environment/launch-env.js
new file mode 100644
index 0000000..73b4bae
--- /dev/null
+++ b/modules/e2e/testcafe/environment/launch-env.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const { startEnv, dropTestDB } = require('./envtools');
+
+startEnv();
+
+process.on('SIGINT', async() => {
+    await dropTestDB();
+
+    process.exit(0);
+});
diff --git a/modules/e2e/testcafe/fixtures/admin-panel.js b/modules/e2e/testcafe/fixtures/admin-panel.js
new file mode 100644
index 0000000..b49a9ce
--- /dev/null
+++ b/modules/e2e/testcafe/fixtures/admin-panel.js
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector} from 'testcafe';
+import {AngularJSSelector} from 'testcafe-angular-selectors';
+import {dropTestDB, insertTestUser, resolveUrl} from '../environment/envtools';
+import {createRegularUser} from '../roles';
+
+const regularUser = createRegularUser();
+
+fixture('Checking admin panel')
+    .before(async() => {
+        await dropTestDB();
+        await insertTestUser();
+    })
+    .beforeEach(async(t) => {
+        await t.useRole(regularUser);
+        await t.navigateTo(resolveUrl('/settings/admin'));
+    })
+    .after(async() => {
+        await dropTestDB();
+    });
+
+const setNotificationsButton = Selector('button').withText('Set user notifications');
+
+test('Testing setting notifications', async(t) => {
+    await t.click(setNotificationsButton);
+
+    await t
+        .expect(Selector('h4').withText(/.*Set user notifications.*/).exists)
+        .ok()
+        .click('.ace_content')
+        .pressKey('ctrl+a delete')
+        .pressKey('t e s t space m e s s a g e')
+        .click(AngularJSSelector.byModel('$ctrl.isShown'))
+        .click('#btn-submit');
+
+    await t
+        .expect(Selector('.wch-notification').innerText)
+        .eql('test message');
+
+    await t.click(setNotificationsButton);
+
+    await t
+        .click('.ace_content')
+        .pressKey('ctrl+a delete')
+        .click(AngularJSSelector.byModel('$ctrl.isShown'))
+        .click('#btn-submit');
+
+    await t
+        .expect(Selector('.wch-notification', { visibilityCheck: false } ).visible)
+        .notOk();
+});
diff --git a/modules/e2e/testcafe/fixtures/auth/forgot-password.js b/modules/e2e/testcafe/fixtures/auth/forgot-password.js
new file mode 100644
index 0000000..f9e2d8b
--- /dev/null
+++ b/modules/e2e/testcafe/fixtures/auth/forgot-password.js
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {dropTestDB, insertTestUser, resolveUrl} from '../../environment/envtools';
+import {errorNotification} from '../../components/notifications';
+import {pageForgotPassword as page} from '../../page-models/pageForgotPassword';
+
+fixture('Password reset')
+    .page(resolveUrl('/forgot-password'))
+    .before(async() => {
+        await dropTestDB();
+        await insertTestUser();
+    })
+    .after(async() => {
+        await dropTestDB();
+    });
+
+test('Incorrect email', async(t) => {
+    await t
+        .typeText(page.email.control, 'aa')
+        .expect(page.email.getError('email').exists).ok('Marks field as invalid');
+});
+
+test('Unknown email', async(t) => {
+    await t
+        .typeText(page.email.control, 'nonexisting@mail.com', {replace: true})
+        .click(page.remindPasswordButton)
+        .expect(errorNotification.withText('Account with that email address does not exists!').exists).ok('Shows global error notification')
+        .expect(page.email.getError('server').exists).ok('Marks input as server-invalid');
+});
+
+// TODO: IGNITE-8028 Implement this test as unit test.
+test.skip('Successful reset', async(t) => {
+    await t
+        .typeText(page.email.control, 'a@a', {replace: true})
+        .click(page.remindPasswordButton)
+        .expect(page.email.getError('server').exists).notOk('No errors happen');
+});
diff --git a/modules/e2e/testcafe/fixtures/auth/logout.js b/modules/e2e/testcafe/fixtures/auth/logout.js
new file mode 100644
index 0000000..118ce28
--- /dev/null
+++ b/modules/e2e/testcafe/fixtures/auth/logout.js
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {dropTestDB, insertTestUser, resolveUrl} from '../../environment/envtools';
+import {createRegularUser} from '../../roles';
+import {userMenu} from '../../components/userMenu';
+import {pageSignin} from '../../page-models/pageSignin';
+
+const user = createRegularUser();
+
+fixture('Logout')
+    .before(async() => {
+        await dropTestDB();
+        await insertTestUser();
+    })
+    .after(async() => {
+        await dropTestDB();
+    });
+
+test('Successful logout', async(t) => {
+    await t.useRole(user).navigateTo(resolveUrl('/settings/profile'));
+    await userMenu.clickOption('Log out');
+    await t.expect(pageSignin.selector.exists).ok('Goes to sign in page after logout');
+});
diff --git a/modules/e2e/testcafe/fixtures/auth/signup-validation-local.js b/modules/e2e/testcafe/fixtures/auth/signup-validation-local.js
new file mode 100644
index 0000000..d26ce33
--- /dev/null
+++ b/modules/e2e/testcafe/fixtures/auth/signup-validation-local.js
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {resolveUrl} from '../../environment/envtools';
+import {pageSignup as page} from '../../page-models/pageSignup';
+
+fixture('Signup validation local').page(resolveUrl('/signup'));
+
+test('Most fields have validation', async(t) => {
+    await page.fillSignupForm({
+        email: 'foobar',
+        password: '1',
+        passwordConfirm: '2',
+        firstName: '  ',
+        lastName: '   ',
+        company: '   ',
+        country: 'Brazil'
+    });
+
+    await t
+        .expect(page.email.getError('email').exists).ok()
+        .expect(page.passwordConfirm.getError('mismatch').exists).ok()
+        .expect(page.firstName.getError('required').exists).ok()
+        .expect(page.lastName.getError('required').exists).ok()
+        .expect(page.company.getError('required').exists).ok();
+});
+
+test('Errors on submit', async(t) => {
+    await t
+        .typeText(page.email.control, 'email@example.com')
+        .click(page.signupButton)
+        .expect(page.password.control.focused).ok()
+        .expect(page.password.getError('required').exists).ok()
+        .typeText(page.password.control, 'Foo')
+        .click(page.signupButton)
+        .expect(page.passwordConfirm.control.focused).ok()
+        .expect(page.passwordConfirm.getError('required').exists).ok();
+});
diff --git a/modules/e2e/testcafe/fixtures/auth/signup.js b/modules/e2e/testcafe/fixtures/auth/signup.js
new file mode 100644
index 0000000..0580355
--- /dev/null
+++ b/modules/e2e/testcafe/fixtures/auth/signup.js
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {dropTestDB, insertTestUser, resolveUrl} from '../../environment/envtools';
+import {pageSignup as page} from '../../page-models/pageSignup';
+import {errorNotification} from '../../components/notifications';
+import {userMenu} from '../../components/userMenu';
+
+fixture('Signup')
+    .page(resolveUrl('/signup'))
+    .before(async() => {
+        await dropTestDB();
+        await insertTestUser();
+    })
+    .after(async() => {
+        await dropTestDB();
+    });
+
+test('Local validation', async(t) => {
+    await page.fillSignupForm({
+        email: 'foobar',
+        password: '1',
+        passwordConfirm: '2',
+        firstName: '  ',
+        lastName: 'Doe',
+        company: 'FooBar',
+        country: 'Brazil'
+    });
+    await t
+        .expect(page.email.getError('email').exists).ok()
+        .expect(page.passwordConfirm.getError('mismatch').exists).ok()
+        .expect(page.firstName.getError('required').exists).ok();
+});
+test('Server validation', async(t) => {
+    await page.fillSignupForm({
+        email: 'a@a',
+        password: '1',
+        passwordConfirm: '1',
+        firstName: 'John',
+        lastName: 'Doe',
+        company: 'FooBar',
+        country: 'Brazil'
+    });
+    await t
+        .click(page.signupButton)
+        .expect(errorNotification.withText('A user with the given email is already registered').exists).ok('Shows global error')
+        .expect(page.email.getError('server').exists).ok('Marks email input as server-invalid');
+});
+test('Successful signup', async(t) => {
+    await page.fillSignupForm({
+        email: 'test@example.com',
+        password: '1',
+        passwordConfirm: '1',
+        firstName: 'John',
+        lastName: 'Doe',
+        company: 'FooBar',
+        country: 'Brazil'
+    });
+    await t
+        .click(page.signupButton)
+        .expect(userMenu.button.textContent).contains('John Doe', 'User gets logged in under chosen full name');
+});
diff --git a/modules/e2e/testcafe/fixtures/configuration/basic.js b/modules/e2e/testcafe/fixtures/configuration/basic.js
new file mode 100644
index 0000000..b431e7b
--- /dev/null
+++ b/modules/e2e/testcafe/fixtures/configuration/basic.js
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {dropTestDB, insertTestUser, resolveUrl} from '../../environment/envtools';
+import {createRegularUser} from '../../roles';
+import {PageConfigurationBasic} from '../../page-models/PageConfigurationBasic';
+import {successNotification} from '../../components/notifications';
+
+const regularUser = createRegularUser();
+
+fixture('Basic configuration')
+    .before(async() => {
+        await dropTestDB();
+        await insertTestUser();
+    })
+    .beforeEach(async(t) => {
+        await t
+            .useRole(regularUser)
+            .navigateTo(resolveUrl('/configuration/new/basic'));
+    })
+    .after(dropTestDB);
+
+test('Off-heap size visibility for different Ignite versions', async(t) => {
+    const page = new PageConfigurationBasic();
+    const ignite2 = 'Ignite 2.4';
+    const ignite1 = 'Ignite 1.x';
+
+    await page.versionPicker.pickVersion(ignite2);
+    await t.expect(page.totalOffheapSizeInput.exists).ok('Visible in latest 2.x version');
+    await page.versionPicker.pickVersion(ignite1);
+    await t.expect(page.totalOffheapSizeInput.count).eql(0, 'Invisible in Ignite 1.x');
+});
+
+test('Default form action', async(t) => {
+    const page = new PageConfigurationBasic();
+
+    await t
+        .expect(page.mainFormAction.textContent)
+        .eql(PageConfigurationBasic.SAVE_CHANGES_AND_DOWNLOAD_LABEL);
+});
+
+test('Basic editing', async(t) => {
+    const page = new PageConfigurationBasic();
+    const clusterName = 'Test basic cluster #1';
+    const localMode = 'LOCAL';
+    const atomic = 'ATOMIC';
+
+    await t
+        .expect(page.buttonPreviewProject.visible).notOk('Preview project button is hidden for new cluster configs')
+        .typeText(page.clusterNameInput.control, clusterName, {replace: true});
+    await page.cachesList.addItem();
+    await page.cachesList.addItem();
+    await page.cachesList.addItem();
+
+    const cache1 = page.cachesList.getItem(1);
+    await cache1.startEdit();
+    await t.typeText(cache1.fields.name.control, 'Foobar');
+    await cache1.fields.cacheMode.selectOption(localMode);
+    await cache1.fields.atomicityMode.selectOption(atomic);
+    await cache1.stopEdit();
+
+    await t.expect(cache1.getItemViewColumn(0).textContent).contains(`Cache1Foobar`, 'Can edit cache name');
+    await t.expect(cache1.getItemViewColumn(1).textContent).eql(localMode, 'Can edit cache mode');
+    await t.expect(cache1.getItemViewColumn(2).textContent).eql(atomic, 'Can edit cache atomicity');
+
+    await page.save();
+    await t
+        .expect(successNotification.visible).ok('Shows success notifications')
+        .expect(successNotification.textContent).contains(`Cluster "${clusterName}" saved.`, 'Success notification has correct text', {timeout: 500});
+    await t.eval(() => window.location.reload());
+    await t.expect(page.pageHeader.textContent).contains(`Edit cluster configuration ‘${clusterName}’`);
+});
diff --git a/modules/e2e/testcafe/fixtures/configuration/clusterFormChangeDetection.js b/modules/e2e/testcafe/fixtures/configuration/clusterFormChangeDetection.js
new file mode 100644
index 0000000..693add4
--- /dev/null
+++ b/modules/e2e/testcafe/fixtures/configuration/clusterFormChangeDetection.js
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {dropTestDB, insertTestUser, resolveUrl} from '../../environment/envtools';
+import {createRegularUser} from '../../roles';
+import {PageConfigurationOverview} from '../../page-models/PageConfigurationOverview';
+import {PageConfigurationAdvancedCluster} from '../../page-models/PageConfigurationAdvancedCluster';
+import {advancedNavButton} from '../../components/pageConfiguration';
+import {pageAdvancedConfiguration} from '../../components/pageAdvancedConfiguration';
+import {confirmation} from '../../components/confirmation';
+import {scrollIntoView} from '../../helpers';
+
+const regularUser = createRegularUser();
+
+fixture('Cluster configuration form change detection')
+    .before(async() => {
+        await dropTestDB();
+        await insertTestUser();
+    })
+    .beforeEach(async(t) => {
+        await t.useRole(regularUser);
+    })
+    .after(dropTestDB);
+
+test.skip('New cluster change detection', async(t) => {
+    const overview = new PageConfigurationOverview();
+    const advanced = new PageConfigurationAdvancedCluster();
+
+    await t
+        .navigateTo(resolveUrl(`/configuration/overview`))
+        .click(overview.createClusterConfigButton)
+        .click(advancedNavButton);
+
+    await t.click(advanced.sections.connectorConfiguration.panel.heading);
+
+    // IODO: Investigate why this code doesn't work in headless mode;
+    await scrollIntoView.with({dependencies: {el: advanced.sections.connectorConfiguration.inputs.enable.control}})();
+
+    await t
+        .click(advanced.sections.connectorConfiguration.inputs.enable.control)
+        .click(advanced.saveButton)
+        .click(pageAdvancedConfiguration.cachesNavButton)
+        .expect(confirmation.body.exists).notOk(`Doesn't show changes confirmation after saving new cluster`);
+});
diff --git a/modules/e2e/testcafe/fixtures/configuration/newClusterWithCache.js b/modules/e2e/testcafe/fixtures/configuration/newClusterWithCache.js
new file mode 100644
index 0000000..d6976f5
--- /dev/null
+++ b/modules/e2e/testcafe/fixtures/configuration/newClusterWithCache.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {dropTestDB, insertTestUser, resolveUrl} from '../../environment/envtools';
+import {createRegularUser} from '../../roles';
+import {PageConfigurationOverview} from '../../page-models/PageConfigurationOverview';
+import {PageConfigurationAdvancedCluster} from '../../page-models/PageConfigurationAdvancedCluster';
+import {configureNavButton} from '../../components/topNavigation';
+
+const regularUser = createRegularUser();
+
+fixture('New cluster with cache')
+    .before(async() => {
+        await dropTestDB();
+        await insertTestUser();
+    })
+    .beforeEach(async(t) => {
+        await t.useRole(regularUser);
+    })
+    .after(dropTestDB);
+
+test(`New cluster name doesn't disappear`, async(t) => {
+    const overview = new PageConfigurationOverview();
+    const advanced = new PageConfigurationAdvancedCluster();
+
+    await t
+        .navigateTo(resolveUrl(`/configuration/new/advanced/caches/new`))
+        .click(advanced.saveButton)
+        .click(configureNavButton)
+        .expect(overview.clustersTable.findCell(0, 'Name').textContent).contains('Cluster');
+});
diff --git a/modules/e2e/testcafe/fixtures/configuration/overview.js b/modules/e2e/testcafe/fixtures/configuration/overview.js
new file mode 100644
index 0000000..7f48ab1
--- /dev/null
+++ b/modules/e2e/testcafe/fixtures/configuration/overview.js
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {getLocationPathname} from '../../helpers';
+import {dropTestDB, insertTestUser, resolveUrl} from '../../environment/envtools';
+import {createRegularUser} from '../../roles';
+import {PageConfigurationOverview} from '../../page-models/PageConfigurationOverview';
+import {PageConfigurationBasic} from '../../page-models/PageConfigurationBasic';
+import * as pageConfiguration from '../../components/pageConfiguration';
+import {pageAdvancedConfiguration} from '../../components/pageAdvancedConfiguration';
+import {PageConfigurationAdvancedCluster} from '../../page-models/PageConfigurationAdvancedCluster';
+import {confirmation} from '../../components/confirmation';
+import {successNotification} from '../../components/notifications';
+import * as models from '../../page-models/pageConfigurationAdvancedModels';
+import * as igfs from '../../page-models/pageConfigurationAdvancedIGFS';
+import {configureNavButton} from '../../components/topNavigation';
+
+const regularUser = createRegularUser();
+
+const repeat = (times, fn) => [...Array(times).keys()].reduce((acc, i) => acc.then(() => fn(i)), Promise.resolve());
+
+fixture('Configuration overview')
+    .before(async(t) => {
+        await dropTestDB();
+        await insertTestUser();
+    })
+    .beforeEach(async(t) => {
+        await t.useRole(regularUser).navigateTo(resolveUrl(`/configuration/overview`));
+    })
+    .after(dropTestDB);
+
+const overviewPage = new PageConfigurationOverview();
+const basicConfigPage = new PageConfigurationBasic();
+const advancedConfigPage = new PageConfigurationAdvancedCluster();
+
+test('Create cluster basic/advanced clusters amount redirect', async(t) => {
+    const clustersAmountThershold = 10;
+
+    await repeat(clustersAmountThershold + 2, async(i) => {
+        await t.click(overviewPage.createClusterConfigButton);
+
+        if (i <= clustersAmountThershold) {
+            await t.expect(getLocationPathname()).contains('basic', 'Opens basic');
+            await basicConfigPage.saveWithoutDownload();
+        } else {
+            await t.expect(getLocationPathname()).contains('advanced', 'Opens advanced');
+            await advancedConfigPage.save();
+        }
+
+        await t.click(configureNavButton);
+    });
+    await overviewPage.removeAllItems();
+});
+
+
+test('Cluster edit basic/advanced redirect based on caches amount', async(t) => {
+    const clusterName = 'Seven caches cluster';
+    const clusterEditLink = overviewPage.clustersTable.findCell(0, 'Name').find('a');
+    const cachesAmountThreshold = 5;
+
+    await t.click(overviewPage.createClusterConfigButton);
+    await repeat(cachesAmountThreshold, () => basicConfigPage.cachesList.addItem());
+    await basicConfigPage.saveWithoutDownload();
+    await t
+        .click(configureNavButton)
+        .click(clusterEditLink)
+        .expect(getLocationPathname()).contains('basic', `Opens basic with ${cachesAmountThreshold} caches`);
+    await basicConfigPage.cachesList.addItem();
+    await basicConfigPage.saveWithoutDownload();
+    await t
+        .click(configureNavButton)
+        .click(clusterEditLink)
+        .expect(getLocationPathname()).contains('advanced', `Opens advanced with ${cachesAmountThreshold + 1} caches`);
+    await t.click(configureNavButton);
+    await overviewPage.removeAllItems();
+});
+
+test('Cluster removal', async(t) => {
+    const name = 'FOO bar BAZ';
+
+    await t
+        .click(overviewPage.createClusterConfigButton)
+        .typeText(basicConfigPage.clusterNameInput.control, name, {replace: true});
+    await basicConfigPage.saveWithoutDownload();
+    await t.click(configureNavButton);
+    await overviewPage.clustersTable.toggleRowSelection(1);
+    await overviewPage.clustersTable.performAction('Delete');
+    await t.expect(confirmation.body.textContent).contains(name, 'Lists cluster names in remove confirmation');
+    await confirmation.confirm();
+    await t.expect(successNotification.textContent).contains('Cluster(s) removed: 1', 'Shows cluster removal notification');
+});
+
+test('Cluster cell values', async(t) => {
+    const name = 'Non-empty cluster config';
+    const staticDiscovery = 'Static IPs';
+    const cachesAmount = 3;
+    const modelsAmount = 2;
+    const igfsAmount = 1;
+
+    await t
+        .click(overviewPage.createClusterConfigButton)
+        .typeText(basicConfigPage.clusterNameInput.control, name, {replace: true});
+    await basicConfigPage.clusterDiscoveryInput.selectOption(staticDiscovery);
+    await repeat(cachesAmount, () => basicConfigPage.cachesList.addItem());
+    await basicConfigPage.saveWithoutDownload();
+    await t
+        .click(pageConfiguration.advancedNavButton)
+        .click(pageAdvancedConfiguration.modelsNavButton);
+    await repeat(modelsAmount, async(i) => {
+        await t
+            .click(models.createModelButton)
+            .click(models.general.generatePOJOClasses.control);
+        await models.general.queryMetadata.selectOption('Annotations');
+        await t
+            .typeText(models.general.keyType.control, `foo${i}`)
+            .typeText(models.general.valueType.control, `bar${i}`)
+            .click(pageAdvancedConfiguration.saveButton);
+    });
+    await t.click(pageAdvancedConfiguration.igfsNavButton);
+    await repeat(igfsAmount, async() => {
+        await t
+            .click(igfs.createIGFSButton)
+            .click(pageAdvancedConfiguration.saveButton);
+    });
+    await t
+        .click(configureNavButton)
+        .expect(overviewPage.clustersTable.findCell(0, 'Name').textContent).contains(name)
+        .expect(overviewPage.clustersTable.findCell(0, 'Discovery').textContent).contains(staticDiscovery)
+        .expect(overviewPage.clustersTable.findCell(0, 'Caches').textContent).contains(cachesAmount)
+        .expect(overviewPage.clustersTable.findCell(0, 'Models').textContent).contains(modelsAmount)
+        .expect(overviewPage.clustersTable.findCell(0, 'IGFS').textContent).contains(igfsAmount);
+});
diff --git a/modules/e2e/testcafe/fixtures/menu-smoke.js b/modules/e2e/testcafe/fixtures/menu-smoke.js
new file mode 100644
index 0000000..615bcd5
--- /dev/null
+++ b/modules/e2e/testcafe/fixtures/menu-smoke.js
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector} from 'testcafe';
+import {dropTestDB, insertTestUser, resolveUrl} from '../environment/envtools';
+import {createRegularUser} from '../roles';
+import {configureNavButton, queriesNavButton} from '../components/topNavigation';
+
+const regularUser = createRegularUser();
+
+fixture('Checking Ingite main menu')
+    .before(async() => {
+        await dropTestDB();
+        await insertTestUser();
+    })
+    .beforeEach(async(t) => {
+        await t.useRole(regularUser);
+        await t.navigateTo(resolveUrl('/'));
+    })
+    .after(async() => {
+        await dropTestDB();
+    });
+
+test('Ignite main menu smoke test', async(t) => {
+    await t
+        .click(configureNavButton)
+        .expect(Selector('title').innerText)
+        .eql('Configuration – Apache Ignite Web Console');
+
+    await t
+        .click(queriesNavButton)
+        .expect(Selector('title').innerText)
+        .eql('Notebooks – Apache Ignite Web Console');
+});
diff --git a/modules/e2e/testcafe/fixtures/queries/notebooks-list.js b/modules/e2e/testcafe/fixtures/queries/notebooks-list.js
new file mode 100644
index 0000000..c7ddc2a
--- /dev/null
+++ b/modules/e2e/testcafe/fixtures/queries/notebooks-list.js
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Selector} from 'testcafe';
+import {dropTestDB, insertTestUser, resolveUrl} from '../../environment/envtools';
+import {createRegularUser} from '../../roles';
+import {PageQueriesNotebooksList} from '../../page-models/PageQueries';
+
+const regularUser = createRegularUser();
+const notebooksListPage = new PageQueriesNotebooksList();
+const notebookName = 'test_notebook';
+
+fixture('Checking Ignite queries notebooks list')
+    .before(async() => {
+        await dropTestDB();
+        await insertTestUser();
+    })
+    .beforeEach(async(t) => {
+        await t.useRole(regularUser);
+        await t.navigateTo(resolveUrl('/queries/notebooks'));
+    })
+    .after(async() => {
+        await dropTestDB();
+    });
+
+test('Testing creating notebook', async(t) => {
+    await notebooksListPage.createNotebook(notebookName);
+
+    await t.expect(Selector('.notebook-name a').withText(notebookName).exists).ok();
+});
+
+test('Testing cloning notebook', async(t) => {
+    await notebooksListPage.cloneNotebook(notebookName);
+
+    await t.expect(Selector('.notebook-name a').withText(`${notebookName}_1`).exists).ok();
+});
+
+test.skip('Testing forbidding of creating notebook with existing name', async(t) => {
+    // Todo: Implement this test after investigation on reason of testcafe blocking notebooks API query.
+});
+
+test('Testing deleting notebooks', async(t) => {
+    await notebooksListPage.deleteAllNotebooks();
+
+    await t.expect(Selector('.notebook-name a').withText(`${notebookName}_1`).exists).notOk();
+});
diff --git a/modules/e2e/testcafe/fixtures/user-profile/credentials.js b/modules/e2e/testcafe/fixtures/user-profile/credentials.js
new file mode 100644
index 0000000..ad944bc
--- /dev/null
+++ b/modules/e2e/testcafe/fixtures/user-profile/credentials.js
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {dropTestDB, insertTestUser, resolveUrl} from '../../environment/envtools';
+import {createRegularUser} from '../../roles';
+import {pageProfile} from '../../page-models/pageProfile';
+import {confirmation} from '../../components/confirmation';
+import {successNotification} from '../../components/notifications';
+
+const regularUser = createRegularUser();
+
+fixture('Checking user credentials change')
+    .before(async() => {
+        await dropTestDB();
+        await insertTestUser();
+    })
+    .beforeEach(async(t) => {
+        await t.useRole(regularUser);
+        await t.navigateTo(resolveUrl('/settings/profile'));
+    })
+    .after(async() => {
+        await dropTestDB();
+    });
+
+test('Testing secure token change', async(t) => {
+    await t.click(pageProfile.securityToken.panel.heading);
+
+    const currentToken = await pageProfile.securityToken.value.control.value;
+
+    await t
+        .click(pageProfile.securityToken.generateTokenButton)
+        .expect(confirmation.body.innerText).contains(
+`Are you sure you want to change security token?
+If you change the token you will need to restart the agent.`
+        )
+        .click(confirmation.confirmButton)
+        .expect(pageProfile.securityToken.value.control.value).notEql(currentToken);
+});
+
+test('Testing password change', async(t) => {
+    const pass = 'newPass';
+
+    await t
+        .click(pageProfile.password.panel.heading)
+        .typeText(pageProfile.password.newPassword.control, pass)
+        .typeText(pageProfile.password.confirmPassword.control, pass)
+        .click(pageProfile.saveChangesButton)
+        .expect(successNotification.withText('Profile saved.').exists).ok();
+});
diff --git a/modules/e2e/testcafe/fixtures/user-profile/profile.js b/modules/e2e/testcafe/fixtures/user-profile/profile.js
new file mode 100644
index 0000000..69be02d
--- /dev/null
+++ b/modules/e2e/testcafe/fixtures/user-profile/profile.js
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {dropTestDB, insertTestUser, resolveUrl} from '../../environment/envtools';
+import {createRegularUser} from '../../roles';
+import {pageProfile} from '../../page-models/pageProfile';
+
+const regularUser = createRegularUser();
+
+fixture('Checking user profile')
+    .before(async() => {
+        await dropTestDB();
+        await insertTestUser();
+    })
+    .beforeEach(async(t) => {
+        await t.useRole(regularUser);
+        await t.navigateTo(resolveUrl('/settings/profile'));
+    })
+    .after(async() => {
+        await dropTestDB();
+    });
+
+test('Testing user data change', async(t) => {
+    const firstName = 'Richard';
+    const lastName = 'Roe';
+    const email = 'r.roe@mail.com';
+    const company = 'New Company';
+    const country = 'Israel';
+
+    await t
+        .typeText(pageProfile.firstName.control, firstName, {replace: true})
+        .typeText(pageProfile.lastName.control, lastName, {replace: true})
+        .typeText(pageProfile.email.control, email, {replace: true})
+        .typeText(pageProfile.company.control, company, {replace: true});
+    await pageProfile.country.selectOption(country);
+    await t.click(pageProfile.saveChangesButton);
+
+    await t
+        .navigateTo(resolveUrl('/settings/profile'))
+        .expect(pageProfile.firstName.control.value).eql(firstName)
+        .expect(pageProfile.lastName.control.value).eql(lastName)
+        .expect(pageProfile.email.control.value).eql(email)
+        .expect(pageProfile.company.control.value).eql(company)
+        .expect(pageProfile.country.control.innerText).eql(country);
+});
diff --git a/modules/e2e/testcafe/helpers.js b/modules/e2e/testcafe/helpers.js
new file mode 100644
index 0000000..908bd6e
--- /dev/null
+++ b/modules/e2e/testcafe/helpers.js
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const { ClientFunction } = require('testcafe');
+
+const mouseenterTrigger = ClientFunction((selector = '') => {
+    return new Promise((resolve) => {
+        window.jQuery(selector).mouseenter();
+
+        resolve();
+    });
+});
+
+const getLocationPathname = ClientFunction(() => Promise.resolve(location.pathname));
+
+/**
+ * Fake visibility predicate, use with selector.filter method.
+ *
+ * @param {Element} node
+ */
+const isVisible = (node) => !!node.getBoundingClientRect().width;
+
+const scrollIntoView = ClientFunction(() => el().scrollIntoView());
+
+module.exports = { mouseenterTrigger, getLocationPathname, isVisible, scrollIntoView };
diff --git a/modules/e2e/testcafe/index.js b/modules/e2e/testcafe/index.js
new file mode 100644
index 0000000..837f2df
--- /dev/null
+++ b/modules/e2e/testcafe/index.js
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const glob = require('glob');
+const argv = require('minimist')(process.argv.slice(2));
+const { startTestcafe } = require('./testcafe-runner');
+
+const enableEnvironment = argv.env;
+
+// See all supported browsers at http://devexpress.github.io/testcafe/documentation/using-testcafe/common-concepts/browsers/browser-support.html#locally-installed-browsers
+const BROWSERS = ['chromium:headless --no-sandbox']; // For example: ['chrome', 'firefox'];
+
+const FIXTURES_PATHS = glob.sync('./fixtures/**/*.js');
+
+const testcafeRunnerConfig = {
+    browsers: BROWSERS,
+    enableEnvironment: enableEnvironment || false,
+    reporter: process.env.REPORTER || 'spec',
+    fixturesPathsArray: FIXTURES_PATHS
+};
+
+startTestcafe(testcafeRunnerConfig).then(() => {
+    process.exit(0);
+});
diff --git a/modules/e2e/testcafe/package.json b/modules/e2e/testcafe/package.json
new file mode 100644
index 0000000..6cba09e
--- /dev/null
+++ b/modules/e2e/testcafe/package.json
@@ -0,0 +1,42 @@
+{
+  "name": "ignite-web-console-e2e-tests",
+  "version": "2.7.0",
+  "description": "E2E tests for Apache Ignite Web console",
+  "private": true,
+  "main": "index.js",
+  "scripts": {
+    "env": "node environment/launch-env.js",
+    "test": "node index.js --env=true"
+  },
+  "license": "Apache-2.0",
+  "keywords": [
+    "Apache Ignite Web console"
+  ],
+  "homepage": "https://ignite.apache.org/",
+  "engines": {
+    "npm": ">=5.x.x",
+    "node": ">=8.x.x <10.x.x"
+  },
+  "os": [
+    "darwin",
+    "linux",
+    "win32"
+  ],
+  "dependencies": {
+    "app-module-path": "2.2.0",
+    "cross-env": "5.1.1",
+    "glob": "7.1.2",
+    "lodash": "4.17.10",
+    "minimist": "1.2.0",
+    "mongodb": "2.2.33",
+    "node-cmd": "3.0.0",
+    "objectid": "3.2.1",
+    "path": "0.12.7",
+    "sinon": "2.3.8",
+    "testcafe": "0.22.0",
+    "testcafe-angular-selectors": "0.3.0",
+    "testcafe-reporter-teamcity": "1.0.9",
+    "type-detect": "4.0.3",
+    "util": "0.10.3"
+  }
+}
diff --git a/modules/e2e/testcafe/page-models/PageConfigurationAdvancedCluster.js b/modules/e2e/testcafe/page-models/PageConfigurationAdvancedCluster.js
new file mode 100644
index 0000000..e326100
--- /dev/null
+++ b/modules/e2e/testcafe/page-models/PageConfigurationAdvancedCluster.js
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector, t} from 'testcafe';
+import {PanelCollapsible} from '../components/PanelCollapsible';
+import {FormField} from '../components/FormField';
+
+export class PageConfigurationAdvancedCluster {
+    constructor() {
+        this.saveButton = Selector('.pc-form-actions-panel .btn-ignite').withText('Save');
+
+        this.sections = {
+            connectorConfiguration: {
+                panel: new PanelCollapsible('Connector configuration'),
+                inputs: {
+                    enable: new FormField({id: 'restEnabledInput'})
+                }
+            }
+        };
+    }
+
+    async save() {
+        await t.click(this.saveButton);
+    }
+}
diff --git a/modules/e2e/testcafe/page-models/PageConfigurationBasic.js b/modules/e2e/testcafe/page-models/PageConfigurationBasic.js
new file mode 100644
index 0000000..e979da0
--- /dev/null
+++ b/modules/e2e/testcafe/page-models/PageConfigurationBasic.js
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector, t} from 'testcafe';
+import {FormField} from '../components/FormField';
+import {ListEditable} from '../components/ListEditable';
+
+class VersionPicker {
+    constructor() {
+        this._selector = Selector('version-picker');
+    }
+    /**
+     * @param {string} label Version label
+     */
+    pickVersion(label) {
+        return t
+            .hover(this._selector)
+            .click(this._selector.find('[role="menuitem"]').withText(label));
+    }
+}
+
+export class PageConfigurationBasic {
+    static SAVE_CHANGES_AND_DOWNLOAD_LABEL = 'Save and Download';
+    static SAVE_CHANGES_LABEL = 'Save';
+
+    constructor() {
+        this._selector = Selector('page-configure-basic');
+        this.versionPicker = new VersionPicker();
+        this.totalOffheapSizeInput = Selector('form-field-size#memory');
+        this.mainFormAction = Selector('.pc-form-actions-panel .btn-ignite-group .btn-ignite:nth-of-type(1)');
+        this.contextFormActionsButton = Selector('.pc-form-actions-panel .btn-ignite-group .btn-ignite:nth-of-type(2)');
+        this.contextSaveButton = Selector('a[role=menuitem]').withText(new RegExp(`^${PageConfigurationBasic.SAVE_CHANGES_LABEL}$`));
+        this.contextSaveAndDownloadButton = Selector('a[role=menuitem]').withText(PageConfigurationBasic.SAVE_CHANGES_AND_DOWNLOAD_LABEL);
+        this.buttonPreviewProject = Selector('button-preview-project');
+        this.clusterNameInput = new FormField({id: 'clusterNameInput'});
+        this.clusterDiscoveryInput = new FormField({id: 'discoveryInput'});
+        this.cachesList = new ListEditable(Selector('.pcb-caches-list'), {
+            name: {id: 'nameInput'},
+            cacheMode: {id: 'cacheModeInput'},
+            atomicityMode: {id: 'atomicityModeInput'},
+            backups: {id: 'backupsInput'}
+        });
+        this.pageHeader = Selector('.header-with-selector h1');
+    }
+
+    async save() {
+        await t.click(this.mainFormAction);
+    }
+
+    async saveWithoutDownload() {
+        return await t.click(this.contextFormActionsButton).click(this.contextSaveButton);
+    }
+}
diff --git a/modules/e2e/testcafe/page-models/PageConfigurationOverview.js b/modules/e2e/testcafe/page-models/PageConfigurationOverview.js
new file mode 100644
index 0000000..652b50f
--- /dev/null
+++ b/modules/e2e/testcafe/page-models/PageConfigurationOverview.js
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector, t} from 'testcafe';
+import {Table} from '../components/Table';
+import {confirmation} from '../components/confirmation';
+import {successNotification} from '../components/notifications';
+
+export class PageConfigurationOverview {
+    constructor() {
+        this.createClusterConfigButton = Selector('.btn-ignite').withText('Create Cluster Configuration');
+        this.importFromDBButton = Selector('.btn-ignite').withText('Import from Database');
+        this.clustersTable = new Table(Selector('pc-items-table'));
+        this.pageHeader = Selector('.pc-page-header');
+    }
+    async removeAllItems() {
+        await t.click(this.clustersTable.allItemsCheckbox);
+        await this.clustersTable.performAction('Delete');
+        await confirmation.confirm();
+        await t.expect(successNotification.visible).ok();
+    }
+}
diff --git a/modules/e2e/testcafe/page-models/PageQueries.js b/modules/e2e/testcafe/page-models/PageQueries.js
new file mode 100644
index 0000000..a33ab75
--- /dev/null
+++ b/modules/e2e/testcafe/page-models/PageQueries.js
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector, t} from 'testcafe';
+import {ModalInput} from '../components/modalInput';
+import {confirmation} from '../components/confirmation';
+import {mouseenterTrigger} from '../helpers';
+import _ from 'lodash';
+
+export class PageQueriesNotebooksList {
+    constructor() {
+        this.createNotebookButton = Selector('#createNotebookBtn');
+        this.createNotebookModal = new ModalInput();
+    }
+
+    async createNotebook(notebookName) {
+        await t.click(this.createNotebookButton);
+
+        await this.createNotebookModal.enterValue(notebookName);
+
+        return this.createNotebookModal.confirm();
+    }
+
+    async selectNotebookByName(notebookName) {
+        const notebookRows = await Selector('.notebook-name a');
+        const notebookRowsIndices = _.range(await notebookRows.count + 1);
+        const notebookRowIndex = notebookRowsIndices.findIndex(async(i) => {
+            return notebookName === await notebookRows.nth(i).innerText;
+        });
+
+        return t.click(Selector('.ui-grid-selection-row-header-buttons').nth(notebookRowIndex + 1).parent());
+    }
+
+    selectAllNotebooks() {
+        return t.click(Selector('.ui-grid-selection-row-header-buttons').nth(0).parent());
+    }
+
+    async deleteAllNotebooks() {
+        await this.selectAllNotebooks();
+
+        await mouseenterTrigger('.btn-ignite:contains(Actions)');
+        await t.click(Selector('a').withText('Delete'));
+
+        return confirmation.confirm();
+    }
+
+    async cloneNotebook(notebookName) {
+        await this.selectNotebookByName(notebookName);
+        await mouseenterTrigger('.btn-ignite:contains(Actions)');
+        await t.click(Selector('a').withText('Clone'));
+
+        return this.createNotebookModal.confirm();
+    }
+}
diff --git a/modules/e2e/testcafe/page-models/pageConfigurationAdvancedIGFS.js b/modules/e2e/testcafe/page-models/pageConfigurationAdvancedIGFS.js
new file mode 100644
index 0000000..8225f7e
--- /dev/null
+++ b/modules/e2e/testcafe/page-models/pageConfigurationAdvancedIGFS.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector} from 'testcafe';
+import {isVisible} from '../helpers';
+
+export const createIGFSButton = Selector('pc-items-table footer-slot .link-success').filter(isVisible);
diff --git a/modules/e2e/testcafe/page-models/pageConfigurationAdvancedModels.js b/modules/e2e/testcafe/page-models/pageConfigurationAdvancedModels.js
new file mode 100644
index 0000000..c9b64b9
--- /dev/null
+++ b/modules/e2e/testcafe/page-models/pageConfigurationAdvancedModels.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector} from 'testcafe';
+import {FormField} from '../components/FormField';
+import {isVisible} from '../helpers';
+
+export const createModelButton = Selector('pc-items-table footer-slot .link-success').filter(isVisible);
+export const general = {
+    generatePOJOClasses: new FormField({id: 'generatePojoInput'}),
+    queryMetadata: new FormField({id: 'queryMetadataInput'}),
+    keyType: new FormField({id: 'keyTypeInput'}),
+    valueType: new FormField({id: 'valueTypeInput'})
+};
diff --git a/modules/e2e/testcafe/page-models/pageForgotPassword.js b/modules/e2e/testcafe/page-models/pageForgotPassword.js
new file mode 100644
index 0000000..3b4160a
--- /dev/null
+++ b/modules/e2e/testcafe/page-models/pageForgotPassword.js
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector} from 'testcafe';
+import {CustomFormField} from '../components/FormField';
+
+export const pageForgotPassword = {
+    email: new CustomFormField({model: '$ctrl.data.email'}),
+    remindPasswordButton: Selector('button').withText('Send it to me')
+};
diff --git a/modules/e2e/testcafe/page-models/pageProfile.js b/modules/e2e/testcafe/page-models/pageProfile.js
new file mode 100644
index 0000000..8a81f36
--- /dev/null
+++ b/modules/e2e/testcafe/page-models/pageProfile.js
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {CustomFormField} from '../components/FormField';
+import {PanelCollapsible} from '../components/PanelCollapsible';
+import {Selector} from 'testcafe';
+
+export const pageProfile = {
+    firstName: new CustomFormField({id: 'firstNameInput'}),
+    lastName: new CustomFormField({id: 'lastNameInput'}),
+    email: new CustomFormField({id: 'emailInput'}),
+    phone: new CustomFormField({id: 'phoneInput'}),
+    country: new CustomFormField({id: 'countryInput'}),
+    company: new CustomFormField({id: 'companyInput'}),
+    securityToken: {
+        panel: new PanelCollapsible('security token'),
+        generateTokenButton: Selector('a').withText('Generate Random Security Token?'),
+        value: new CustomFormField({id: 'securityTokenInput'})
+    },
+    password: {
+        panel: new PanelCollapsible('password'),
+        newPassword: new CustomFormField({id: 'passwordInput'}),
+        confirmPassword: new CustomFormField({id: 'passwordConfirmInput'})
+    },
+    saveChangesButton: Selector('.btn-ignite.btn-ignite--success').withText('Save Changes')
+};
diff --git a/modules/e2e/testcafe/page-models/pageSignin.js b/modules/e2e/testcafe/page-models/pageSignin.js
new file mode 100644
index 0000000..57031b6
--- /dev/null
+++ b/modules/e2e/testcafe/page-models/pageSignin.js
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector, t} from 'testcafe';
+import {CustomFormField} from '../components/FormField';
+
+export const pageSignin = {
+    email: new CustomFormField({model: '$ctrl.data.email'}),
+    password: new CustomFormField({model: '$ctrl.data.password'}),
+    signinButton: Selector('button').withText('Sign In'),
+    selector: Selector('page-signin'),
+    async login(email, password) {
+        return await t
+            .typeText(this.email.control, email)
+            .typeText(this.password.control, password)
+            .click(this.signinButton);
+    }
+};
diff --git a/modules/e2e/testcafe/page-models/pageSignup.js b/modules/e2e/testcafe/page-models/pageSignup.js
new file mode 100644
index 0000000..83f2932
--- /dev/null
+++ b/modules/e2e/testcafe/page-models/pageSignup.js
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector, t} from 'testcafe';
+import {CustomFormField} from '../components/FormField';
+
+export const pageSignup = {
+    email: new CustomFormField({id: 'emailInput'}),
+    password: new CustomFormField({id: 'passwordInput'}),
+    passwordConfirm: new CustomFormField({id: 'confirmInput'}),
+    firstName: new CustomFormField({id: 'firstNameInput'}),
+    lastName: new CustomFormField({id: 'lastNameInput'}),
+    company: new CustomFormField({id: 'companyInput'}),
+    country: new CustomFormField({id: 'countryInput'}),
+    signupButton: Selector('button').withText('Sign Up'),
+    async fillSignupForm({
+        email,
+        password,
+        passwordConfirm,
+        firstName,
+        lastName,
+        company,
+        country
+    }) {
+        await t
+            .typeText(this.email.control, email, {replace: true})
+            .typeText(this.password.control, password, {replace: true})
+            .typeText(this.passwordConfirm.control, passwordConfirm, {replace: true})
+            .typeText(this.firstName.control, firstName, {replace: true})
+            .typeText(this.lastName.control, lastName, {replace: true})
+            .typeText(this.company.control, company, {replace: true});
+        await this.country.selectOption(country);
+    }
+};
diff --git a/modules/e2e/testcafe/roles.js b/modules/e2e/testcafe/roles.js
new file mode 100644
index 0000000..c4b2e3b
--- /dev/null
+++ b/modules/e2e/testcafe/roles.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Role, t} from 'testcafe';
+import {resolveUrl} from './environment/envtools';
+import {pageSignin as page} from './page-models/pageSignin';
+
+export const createRegularUser = () => {
+    return Role(resolveUrl('/signin'), async() => {
+        await t.eval(() => window.localStorage.clear());
+
+        // Disable "Getting started" modal.
+        await t.eval(() => window.localStorage.showGettingStarted = 'false');
+        await page.login('a@a', 'a');
+    });
+};
diff --git a/modules/e2e/testcafe/testcafe-runner.js b/modules/e2e/testcafe/testcafe-runner.js
new file mode 100644
index 0000000..eab767c
--- /dev/null
+++ b/modules/e2e/testcafe/testcafe-runner.js
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const { startEnv, dropTestDB, insertTestUser } = require('./environment/envtools');
+
+const createTestCafe = require('testcafe');
+
+let testcafe = null;
+
+const startTestcafe = (config) => {
+    return createTestCafe('localhost', 1337, 1338)
+        .then(async(tc) => {
+            try {
+                if (config.enableEnvironment)
+                    await startEnv();
+
+                await dropTestDB();
+                await insertTestUser();
+
+                testcafe = tc;
+
+                const runner = testcafe.createRunner();
+
+                console.log('Start E2E testing!');
+
+                return runner
+                    .src(config.fixturesPathsArray)
+                    .browsers(config.browsers)
+                    .reporter(config.reporter)
+                    .run({ skipJsErrors: true });
+            }
+            catch (err) {
+                console.log(err);
+
+                process.exit(1);
+            }
+        })
+        .then(async(failedCount) => {
+            console.log('Cleaning after tests...');
+
+            testcafe.close();
+
+            await dropTestDB();
+
+            console.log('Tests failed: ' + failedCount);
+        });
+};
+
+module.exports = { startTestcafe };
diff --git a/modules/e2e/testenv/Dockerfile b/modules/e2e/testenv/Dockerfile
new file mode 100644
index 0000000..749a50d
--- /dev/null
+++ b/modules/e2e/testenv/Dockerfile
@@ -0,0 +1,51 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+FROM node:10-alpine
+
+ENV NPM_CONFIG_LOGLEVEL error
+
+# Update package list & install.
+RUN apk add --no-cache nginx
+
+# Install global node packages.
+RUN npm install -g pm2
+
+# Copy nginx config.
+COPY e2e/testenv/nginx/nginx.conf /etc/nginx/nginx.conf
+COPY e2e/testenv/nginx/web-console.conf /etc/nginx/web-console.conf
+
+WORKDIR /opt/web-console
+
+# Install node modules for frontend and backend modules.
+COPY backend/package*.json backend/
+RUN (cd backend && npm install --no-optional --production)
+
+COPY frontend/package*.json frontend/
+RUN (cd frontend && npm install --no-optional)
+
+# Copy source.
+COPY backend backend
+COPY frontend frontend
+
+RUN (cd frontend && npm run build)
+
+EXPOSE 9001
+
+WORKDIR /opt/web-console/backend
+
+CMD nginx && pm2-runtime index.js -n web-console-backend
diff --git a/modules/e2e/testenv/nginx/nginx.conf b/modules/e2e/testenv/nginx/nginx.conf
new file mode 100644
index 0000000..169b334
--- /dev/null
+++ b/modules/e2e/testenv/nginx/nginx.conf
@@ -0,0 +1,55 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+user nginx;
+worker_processes 1;
+
+error_log  /var/log/nginx/error.log  warn;
+pid        /var/run/nginx.pid;
+
+events {
+  worker_connections  128;
+}
+
+http {
+  server_tokens off;
+  sendfile            on;
+  tcp_nopush          on;
+
+  keepalive_timeout   60;
+  tcp_nodelay         on;
+
+  client_max_body_size 100m;
+
+  #access log
+  log_format main '$http_host $remote_addr - $remote_user [$time_local] '
+  '"$request" $status $bytes_sent '
+  '"$http_referer" "$http_user_agent" '
+  '"$gzip_ratio"';
+
+  include /etc/nginx/mime.types;
+  default_type  application/octet-stream;
+  gzip on;
+  gzip_disable "msie6";
+  gzip_types text/plain text/css text/xml text/javascript application/json application/x-javascript application/xml application/xml+rss application/javascript;
+  gzip_vary on;
+  gzip_comp_level 5;
+
+  access_log  /var/log/nginx/access.log  main;
+  #conf.d
+  include web-console.conf ;
+}
diff --git a/modules/e2e/testenv/nginx/web-console.conf b/modules/e2e/testenv/nginx/web-console.conf
new file mode 100644
index 0000000..c57c0d4
--- /dev/null
+++ b/modules/e2e/testenv/nginx/web-console.conf
@@ -0,0 +1,62 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+upstream backend-api {
+  server localhost:3000;
+}
+
+server {
+  listen 9001;
+  server_name _;
+
+  set $ignite_console_dir /opt/web-console/frontend/build;
+
+  root $ignite_console_dir;
+
+  error_page 500 502 503 504 /50x.html;
+
+  location / {
+    try_files $uri /index.html = 404;
+  }
+
+  location /api/v1 {
+    proxy_set_header Host $http_host;
+    proxy_pass http://backend-api;
+  }
+
+  location /socket.io {
+    proxy_set_header Upgrade $http_upgrade;
+    proxy_set_header Connection "upgrade";
+    proxy_http_version 1.1;
+    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    proxy_set_header Host $host;
+    proxy_pass http://backend-api;
+  }
+
+  location /agents {
+    proxy_set_header Upgrade $http_upgrade;
+    proxy_set_header Connection "upgrade";
+    proxy_http_version 1.1;
+    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    proxy_set_header Host $host;
+    proxy_pass http://backend-api;
+  }
+
+  location = /50x.html {
+    root $ignite_console_dir/error_page;
+  }
+}
diff --git a/modules/frontend/.babelrc b/modules/frontend/.babelrc
new file mode 100644
index 0000000..b68592c
--- /dev/null
+++ b/modules/frontend/.babelrc
@@ -0,0 +1,16 @@
+{
+    "presets": [
+        ["@babel/env", {
+            "targets": {
+                "browsers": [">1%", "not ie 11", "not op_mini all"]
+            }
+        }],
+        "@babel/preset-typescript"
+    ],
+    "plugins": [
+        ["@babel/plugin-proposal-class-properties", { "loose" : true }],
+        "@babel/plugin-proposal-object-rest-spread",
+        "@babel/plugin-syntax-dynamic-import",
+        "@babel/plugin-transform-parameters"
+    ]
+}
\ No newline at end of file
diff --git a/modules/frontend/.eslintrc b/modules/frontend/.eslintrc
new file mode 100644
index 0000000..0cfc9a2
--- /dev/null
+++ b/modules/frontend/.eslintrc
@@ -0,0 +1,198 @@
+parser: '@typescript-eslint/parser'
+
+plugins:
+    - '@typescript-eslint'
+
+env:
+    es6: true
+    browser: true
+    mocha: true
+parserOptions:
+    sourceType: module
+    ecmaFeatures:
+        arrowFunctions: true
+        blockBindings: true
+        classes: true
+        defaultParams: true
+        destructuring: true
+        module: true
+        objectLiteralComputedProperties: true
+        objectLiteralShorthandMethods: true
+        objectLiteralShorthandProperties: true
+        spread: true
+        templateStrings: true
+        experimentalObjectRestSpread: true
+
+globals:
+    _: true
+    $: true
+    d3: true
+    io: true
+    window: true
+    global: true
+    angular: true
+    saveAs: true
+    process: true
+    require: true
+
+rules:
+    arrow-parens: [1, "always"]
+    arrow-spacing: [1, { "before": true, "after": true }]
+    accessor-pairs: 2
+    block-scoped-var: 2
+    brace-style: [0, "1tbs"]
+    comma-dangle: [2, "never"]
+    comma-spacing: [2, {"before": false, "after": true}]
+    comma-style: [2, "last"]
+    complexity: [1, 40]
+    computed-property-spacing: [2, "never"]
+    consistent-return: 0
+    consistent-this: [0, "that"]
+    constructor-super: 2
+    curly: [2, "multi-or-nest"]
+    default-case: 2
+    dot-location: 0
+    dot-notation: [2, { "allowKeywords": true }]
+    eol-last: 2
+    eqeqeq: 2
+    func-names: 0
+    func-style: [0, "declaration"]
+    generator-star-spacing: 0
+    guard-for-in: 1
+    handle-callback-err: 0
+    id-length: [2, {"min": 1, "max": 60}]
+    indent: [2, 4, {"SwitchCase": 1, "MemberExpression": "off", "CallExpression": {"arguments": "off"}}]
+    key-spacing: [2, { "beforeColon": false, "afterColon": true }]
+    lines-around-comment: 0
+    linebreak-style: [0, "unix"]
+    max-depth: [0, 4]
+    max-len: [0, 120, 4]
+    max-nested-callbacks: [1, 4]
+    max-params: [0, 3]
+    max-statements: [0, 10]
+    new-cap: 2
+    new-parens: 2
+    no-alert: 2
+    no-array-constructor: 2
+    no-bitwise: 0
+    no-caller: 2
+    no-catch-shadow: 2
+    no-cond-assign: 2
+    no-console: 0
+    no-constant-condition: 2
+    no-continue: 0
+    no-class-assign: 2
+    no-const-assign: 2
+    no-control-regex: 2
+    no-debugger: 2
+    no-delete-var: 2
+    no-div-regex: 0
+    no-dupe-keys: 2
+    no-dupe-args: 2
+    no-duplicate-case: 2
+    no-else-return: 2
+    no-empty: 2
+    no-empty-character-class: 2
+    no-eq-null: 2
+    no-eval: 2
+    no-ex-assign: 2
+    no-extend-native: 2
+    no-extra-bind: 2
+    no-extra-boolean-cast: 2
+    no-extra-parens: 0
+    no-extra-semi: 2
+    no-fallthrough: 2
+    no-floating-decimal: 1
+    no-func-assign: 2
+    no-implied-eval: 2
+    no-inline-comments: 0
+    no-inner-declarations: [2, "functions"]
+    no-invalid-regexp: 2
+    no-irregular-whitespace: 2
+    no-iterator: 2
+    no-label-var: 2
+    no-labels: 2
+    no-lone-blocks: 2
+    no-lonely-if: 2
+    no-implicit-coercion: [2, {"boolean": false, "number": true, "string": true}]
+    no-loop-func: 2
+    no-mixed-requires: [0, false]
+    no-mixed-spaces-and-tabs: [2, true]
+    no-multi-spaces: ["error", {"exceptions": { "VariableDeclarator": true }}]
+    no-multi-str: 2
+    no-multiple-empty-lines: [0, {"max": 2}]
+    no-native-reassign: 2
+    no-negated-in-lhs: 2
+    no-nested-ternary: 0
+    no-new: 2
+    no-new-func: 2
+    no-new-object: 2
+    no-new-require: 0
+    no-new-wrappers: 2
+    no-obj-calls: 2
+    no-octal: 2
+    no-octal-escape: 2
+    no-param-reassign: 0
+    no-path-concat: 0
+    no-plusplus: 0
+    no-process-env: 0
+    no-process-exit: 0
+    no-proto: 2
+    no-redeclare: 2
+    no-regex-spaces: 1
+    no-restricted-modules: 0
+    no-script-url: 0
+    no-self-compare: 2
+    no-sequences: 2
+    no-shadow: 0
+    no-shadow-restricted-names: 2
+    no-spaced-func: 2
+    no-sparse-arrays: 1
+    no-sync: 0
+    no-ternary: 0
+    no-trailing-spaces: ["error", {"ignoreComments": true}]
+    no-throw-literal: 0
+    no-this-before-super: 2
+    no-unexpected-multiline: 2
+    // The rule produces undesired results with TS
+    // no-undef: 2
+    no-undef-init: 2
+    no-undefined: 2
+    no-unneeded-ternary: 2
+    no-unreachable: 2
+    no-unused-expressions: [2, { allowShortCircuit: true }]
+    no-unused-vars: [0, {"vars": "all", "args": "after-used"}]
+    typescript/no-unused-vars: [0]
+    no-useless-call: 2
+    no-void: 0
+    no-var: 2
+    no-warning-comments: 0
+    no-with: 2
+    newline-after-var: 0
+    object-shorthand: [2, "always"]
+    one-var: [2, "never"]
+    operator-assignment: [2, "always"]
+    operator-linebreak: 0
+    padded-blocks: 0
+    prefer-const: 1
+    prefer-spread: 2
+    quote-props: [2, "as-needed"]
+    quotes: [2, "single", {"allowTemplateLiterals": true}]
+    radix: 1
+    semi: [2, "always"]
+    semi-spacing: [2, {"before": false, "after": true}]
+    sort-vars: 0
+    keyword-spacing: 2
+    space-before-blocks: [2, "always"]
+    space-before-function-paren: [2, "never"]
+    space-in-parens: 0
+    space-infix-ops: 2
+    space-unary-ops: [2, { "words": true, "nonwords": false }]
+    spaced-comment: [1, "always", {"markers": ["/"]}]
+    use-isnan: 2
+    valid-jsdoc: 0
+    valid-typeof: 2
+    vars-on-top: 2
+    wrap-iife: 0
+    wrap-regex: 0
+    yoda: [2, "never"]
diff --git a/modules/frontend/.gitignore b/modules/frontend/.gitignore
new file mode 100644
index 0000000..22a01f4
--- /dev/null
+++ b/modules/frontend/.gitignore
@@ -0,0 +1,6 @@
+*.log.*
+.npmrc
+public/stylesheets/*.css
+build/
+node_modules/
+!package-lock.json
\ No newline at end of file
diff --git a/modules/frontend/app/app.config.js b/modules/frontend/app/app.config.js
new file mode 100644
index 0000000..b40add5
--- /dev/null
+++ b/modules/frontend/app/app.config.js
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import negate from 'lodash/negate';
+import isNil from 'lodash/isNil';
+import isEmpty from 'lodash/isEmpty';
+import mixin from 'lodash/mixin';
+
+import {register as registerStore, user as userAction} from './store';
+import alertTemplateUrl from 'views/templates/alert.tpl.pug';
+import dropdownTemplateUrl from 'views/templates/dropdown.tpl.pug';
+import validationTemplateUrl from 'views/templates/validation-error.tpl.pug';
+
+const nonNil = negate(isNil);
+const nonEmpty = negate(isEmpty);
+
+mixin({
+    nonNil,
+    nonEmpty
+});
+
+const igniteConsoleCfg = angular.module('ignite-console.config', ['ngAnimate', 'mgcrea.ngStrap']);
+
+igniteConsoleCfg.run(registerStore);
+
+// Configure AngularJS animation: do not animate fa-spin.
+igniteConsoleCfg.config(['$animateProvider', ($animateProvider) => {
+    $animateProvider.classNameFilter(/^((?!(fa-spin|ng-animate-disabled)).)*$/);
+}]);
+
+// AngularStrap modal popup configuration.
+igniteConsoleCfg.config(['$modalProvider', ($modalProvider) => {
+    Object.assign($modalProvider.defaults, {
+        animation: 'am-fade-and-scale',
+        placement: 'center',
+        html: true
+    });
+}]);
+
+// AngularStrap popover configuration.
+igniteConsoleCfg.config(['$popoverProvider', ($popoverProvider) => {
+    Object.assign($popoverProvider.defaults, {
+        trigger: 'manual',
+        placement: 'right',
+        container: 'body',
+        templateUrl: validationTemplateUrl
+    });
+}]);
+
+// AngularStrap tooltips configuration.
+igniteConsoleCfg.config(['$tooltipProvider', ($tooltipProvider) => {
+    Object.assign($tooltipProvider.defaults, {
+        container: 'body',
+        delay: {show: 150, hide: 150},
+        placement: 'right',
+        html: 'true',
+        trigger: 'click hover'
+    });
+}]);
+
+// AngularStrap select (combobox) configuration.
+igniteConsoleCfg.config(['$selectProvider', ($selectProvider) => {
+    Object.assign($selectProvider.defaults, {
+        container: 'body',
+        maxLength: '5',
+        allText: 'Select All',
+        noneText: 'Clear All',
+        template: '<bs-select-menu></bs-select-menu>',
+        iconCheckmark: 'fa fa-check',
+        caretHtml: '',
+        animation: ''
+    });
+}]);
+
+// AngularStrap alerts configuration.
+igniteConsoleCfg.config(['$alertProvider', ($alertProvider) => {
+    Object.assign($alertProvider.defaults, {
+        container: 'body',
+        placement: 'top-right',
+        duration: '5',
+        templateUrl: alertTemplateUrl,
+        type: 'danger'
+    });
+}]);
+
+
+// AngularStrap dropdowns () configuration.
+igniteConsoleCfg.config(['$dropdownProvider', ($dropdownProvider) => {
+    Object.assign($dropdownProvider.defaults, {
+        templateUrl: dropdownTemplateUrl,
+        animation: ''
+    });
+}]);
+
+// AngularStrap dropdowns () configuration.
+igniteConsoleCfg.config(['$datepickerProvider', ($datepickerProvider) => {
+    Object.assign($datepickerProvider.defaults, {
+        autoclose: true,
+        iconLeft: 'icon-datepicker-left',
+        iconRight: 'icon-datepicker-right'
+    });
+}]);
+
+igniteConsoleCfg.config(['$translateProvider', ($translateProvider) => {
+    $translateProvider.useSanitizeValueStrategy('sanitize');
+}]);
+
+// Restores pre 4.3.0 ui-grid getSelectedRows method behavior
+// ui-grid 4.4+ getSelectedRows additionally skips entries without $$hashKey,
+// which breaks most of out code that works with selected rows.
+igniteConsoleCfg.directive('uiGridSelection', function() {
+    function legacyGetSelectedRows() {
+        return this.rows.filter((row) => row.isSelected).map((row) => row.entity);
+    }
+    return {
+        require: '^uiGrid',
+        restrict: 'A',
+        link(scope, el, attr, ctrl) {
+            ctrl.grid.api.registerMethodsFromObject({selection: {legacyGetSelectedRows}});
+        }
+    };
+});
+
+igniteConsoleCfg.run(['$rootScope', 'Store', ($root, store) => {
+    $root.$on('user', (event, user) => store.dispatch(userAction({...user})));
+}]);
diff --git a/modules/frontend/app/app.d.ts b/modules/frontend/app/app.d.ts
new file mode 100644
index 0000000..3f837ee
--- /dev/null
+++ b/modules/frontend/app/app.d.ts
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+declare module '*.pug' {
+    const pug: string;
+    export default pug;
+}
+declare module '*.scss' {
+    const scss: any;
+    export default scss;
+}
+declare module '*.json' {
+    const value: any;
+    export default value;
+}
diff --git a/modules/frontend/app/app.js b/modules/frontend/app/app.js
new file mode 100644
index 0000000..499c5b0
--- /dev/null
+++ b/modules/frontend/app/app.js
@@ -0,0 +1,385 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+import './style.scss';
+
+import './vendor';
+import '../public/stylesheets/style.scss';
+import '../app/primitives';
+
+import './app.config';
+
+import './modules/form/form.module';
+import './modules/agent/agent.module';
+import './modules/nodes/nodes.module';
+import './modules/demo/Demo.module';
+
+import './modules/states/logout.state';
+import './modules/states/admin.state';
+import './modules/states/errors.state';
+import './modules/states/settings.state';
+// ignite:modules
+import './core';
+import './modules/user/user.module';
+import './modules/branding/branding.module';
+import './modules/navbar/navbar.module';
+import './modules/getting-started/GettingStarted.provider';
+import './modules/dialog/dialog.module';
+import './modules/ace.module';
+import './modules/loading/loading.module';
+import servicesModule from './services';
+import igniteServices from './services';
+// Data
+import i18n from './data/i18n';
+// Directives.
+import igniteAutoFocus from './directives/auto-focus.directive';
+import igniteCopyToClipboard from './directives/copy-to-clipboard.directive';
+import igniteHideOnStateChange from './directives/hide-on-state-change/hide-on-state-change.directive';
+import igniteMatch from './directives/match.directive';
+import igniteOnClickFocus from './directives/on-click-focus.directive';
+import igniteOnEnter from './directives/on-enter.directive';
+import igniteOnEnterFocusMove from './directives/on-enter-focus-move.directive';
+import igniteOnEscape from './directives/on-escape.directive';
+import igniteOnFocusOut from './directives/on-focus-out.directive';
+import igniteRetainSelection from './directives/retain-selection.directive';
+import btnIgniteLink from './directives/btn-ignite-link';
+// Services.
+import ChartColors from './services/ChartColors.service';
+import {Confirm, default as IgniteConfirm} from './services/Confirm.service';
+import ConfirmBatch from './services/ConfirmBatch.service';
+import CopyToClipboard from './services/CopyToClipboard.service';
+import Countries from './services/Countries.service';
+import ErrorPopover from './services/ErrorPopover.service';
+import Focus from './services/Focus.service';
+import FormUtils from './services/FormUtils.service';
+import InetAddress from './services/InetAddress.service';
+import JavaTypes from './services/JavaTypes.service';
+import SqlTypes from './services/SqlTypes.service';
+import LegacyTable from './services/LegacyTable.service';
+import LegacyUtils from './services/LegacyUtils.service';
+import Messages from './services/Messages.service';
+import ErrorParser from './services/ErrorParser.service';
+import ModelNormalizer from './services/ModelNormalizer.service';
+import {CSV} from './services/CSV';
+import {$exceptionHandler} from './services/exceptionHandler';
+
+import {Store} from './services/store';
+
+import AngularStrapTooltip from './services/AngularStrapTooltip.decorator';
+import AngularStrapSelect from './services/AngularStrapSelect.decorator';
+// Filters.
+import byName from './filters/byName.filter';
+import bytes from './filters/bytes.filter';
+import defaultName from './filters/default-name.filter';
+import domainsValidation from './filters/domainsValidation.filter';
+import duration from './filters/duration.filter';
+import hasPojo from './filters/hasPojo.filter';
+import uiGridSubcategories from './filters/uiGridSubcategories.filter';
+import id8 from './filters/id8.filter';
+// Components
+import igniteListOfRegisteredUsers from './components/list-of-registered-users';
+import dialogAdminCreateUser from './components/dialog-admin-create-user';
+import IgniteActivitiesUserDialog from './components/activities-user-dialog';
+import './components/input-dialog';
+import webConsoleHeader from './components/web-console-header';
+import webConsoleFooter from './components/web-console-footer';
+import igniteIcon from './components/ignite-icon';
+import versionPicker from './components/version-picker';
+import userNotifications from './components/user-notifications';
+import pageAdmin from './components/page-admin';
+import pageQueries from './components/page-queries';
+import gridColumnSelector from './components/grid-column-selector';
+import gridItemSelected from './components/grid-item-selected';
+import gridNoData from './components/grid-no-data';
+import gridExport from './components/grid-export';
+import gridShowingRows from './components/grid-showing-rows';
+import bsSelectMenu from './components/bs-select-menu';
+import protectFromBsSelectRender from './components/protect-from-bs-select-render';
+import uiGrid from './components/ui-grid';
+import uiGridHovering from './components/ui-grid-hovering';
+import uiGridFilters from './components/ui-grid-filters';
+import uiGridColumnResizer from './components/ui-grid-column-resizer';
+import listEditable from './components/list-editable';
+import breadcrumbs from './components/breadcrumbs';
+import panelCollapsible from './components/panel-collapsible';
+import clusterSelector from './components/cluster-selector';
+import connectedClusters from './components/connected-clusters-badge';
+import connectedClustersDialog from './components/connected-clusters-dialog';
+import pageLanding from './components/page-landing';
+import passwordVisibility from './components/password-visibility';
+import progressLine from './components/progress-line';
+import formField from './components/form-field';
+import igniteChart from './components/ignite-chart';
+import igniteChartSelector from './components/ignite-chart-series-selector';
+import statusOutput from './components/status-output';
+import timedRedirection from './components/timed-redirection';
+
+import pageProfile from './components/page-profile';
+import pagePasswordChanged from './components/page-password-changed';
+import pagePasswordReset from './components/page-password-reset';
+import pageSignup from './components/page-signup';
+import pageSignin from './components/page-signin';
+import pageForgotPassword from './components/page-forgot-password';
+import formSignup from './components/form-signup';
+import sidebar from './components/web-console-sidebar';
+import permanentNotifications from './components/permanent-notifications';
+import signupConfirmation from './components/page-signup-confirmation';
+import noDataCmp from './components/no-data';
+import globalProgressBar from './components/global-progress-line';
+
+import baseTemplate from 'views/base.pug';
+import * as icons from '../public/images/icons';
+// endignite
+
+export default angular
+    .module('ignite-console', [
+        // Optional AngularJS modules.
+        'ngAnimate',
+        'ngSanitize',
+        'ngMessages',
+        // Third party libs.
+        'asyncFilter',
+        'dndLists',
+        'gridster',
+        'mgcrea.ngStrap',
+        'nvd3',
+        'pascalprecht.translate',
+        'smart-table',
+        'treeControl',
+        'ui.grid',
+        'ui.grid.autoResize',
+        'ui.grid.exporter',
+        'ui.grid.resizeColumns',
+        'ui.grid.saveState',
+        'ui.grid.selection',
+        'ui.router',
+        // Base modules.
+        'ignite-console.core',
+        'ignite-console.ace',
+        'ignite-console.Form',
+        'ignite-console.input-dialog',
+        'ignite-console.user',
+        'ignite-console.branding',
+        'ignite-console.agent',
+        'ignite-console.nodes',
+        'ignite-console.demo',
+        // States.
+        'ignite-console.states.logout',
+        'ignite-console.states.admin',
+        'ignite-console.states.errors',
+        'ignite-console.states.settings',
+        // Common modules.
+        'ignite-console.dialog',
+        'ignite-console.navbar',
+        'ignite-console.getting-started',
+        'ignite-console.loading',
+        // Ignite configuration module.
+        'ignite-console.config',
+        // Components
+        webConsoleHeader.name,
+        webConsoleFooter.name,
+        igniteIcon.name,
+        igniteServices.name,
+        versionPicker.name,
+        userNotifications.name,
+        pageAdmin.name,
+        pageQueries.name,
+        gridColumnSelector.name,
+        gridItemSelected.name,
+        gridNoData.name,
+        gridExport.name,
+        gridShowingRows.name,
+        bsSelectMenu.name,
+        uiGrid.name,
+        uiGridHovering.name,
+        uiGridFilters.name,
+        uiGridColumnResizer.name,
+        protectFromBsSelectRender.name,
+        AngularStrapTooltip.name,
+        AngularStrapSelect.name,
+        listEditable.name,
+        panelCollapsible.name,
+        clusterSelector.name,
+        servicesModule.name,
+        connectedClusters.name,
+        connectedClustersDialog.name,
+        igniteListOfRegisteredUsers.name,
+        dialogAdminCreateUser.name,
+        pageProfile.name,
+        pageLanding.name,
+        pagePasswordChanged.name,
+        pagePasswordReset.name,
+        pageSignup.name,
+        pageSignin.name,
+        pageForgotPassword.name,
+        breadcrumbs.name,
+        passwordVisibility.name,
+        igniteChart.name,
+        igniteChartSelector.name,
+        statusOutput.name,
+        progressLine.name,
+        formField.name,
+        formSignup.name,
+        timedRedirection.name,
+        sidebar.name,
+        permanentNotifications.name,
+        timedRedirection.name,
+        signupConfirmation.name,
+        noDataCmp.name,
+        globalProgressBar.name
+    ])
+    .service('$exceptionHandler', $exceptionHandler)
+    // Directives.
+    .directive('igniteAutoFocus', igniteAutoFocus)
+    .directive('igniteCopyToClipboard', igniteCopyToClipboard)
+    .directive('hideOnStateChange', igniteHideOnStateChange)
+    .directive('igniteMatch', igniteMatch)
+    .directive('igniteOnClickFocus', igniteOnClickFocus)
+    .directive('igniteOnEnter', igniteOnEnter)
+    .directive('igniteOnEnterFocusMove', igniteOnEnterFocusMove)
+    .directive('igniteOnEscape', igniteOnEscape)
+    .directive('igniteRetainSelection', igniteRetainSelection)
+    .directive('igniteOnFocusOut', igniteOnFocusOut)
+    .directive('btnIgniteLinkDashedSuccess', btnIgniteLink)
+    .directive('btnIgniteLinkDashedSecondary', btnIgniteLink)
+    // Services.
+    .service('IgniteErrorPopover', ErrorPopover)
+    .service('JavaTypes', JavaTypes)
+    .service('SqlTypes', SqlTypes)
+    .service('IgniteChartColors', ChartColors)
+    .service('IgniteConfirm', IgniteConfirm)
+    .service('Confirm', Confirm)
+    .service('IgniteConfirmBatch', ConfirmBatch)
+    .service('IgniteCopyToClipboard', CopyToClipboard)
+    .service('IgniteCountries', Countries)
+    .service('IgniteFocus', Focus)
+    .service('IgniteInetAddress', InetAddress)
+    .service('IgniteMessages', Messages)
+    .service('IgniteErrorParser', ErrorParser)
+    .service('IgniteModelNormalizer', ModelNormalizer)
+    .service('IgniteLegacyTable', LegacyTable)
+    .service('IgniteFormUtils', FormUtils)
+    .service('IgniteLegacyUtils', LegacyUtils)
+    .service('IgniteActivitiesUserDialog', IgniteActivitiesUserDialog)
+    .service('CSV', CSV)
+    .service('Store', Store)
+    // Filters.
+    .filter('byName', byName)
+    .filter('bytes', bytes)
+    .filter('defaultName', defaultName)
+    .filter('domainsValidation', domainsValidation)
+    .filter('duration', duration)
+    .filter('hasPojo', hasPojo)
+    .filter('uiGridSubcategories', uiGridSubcategories)
+    .filter('id8', id8)
+    .config(['$translateProvider', '$stateProvider', '$locationProvider', '$urlRouterProvider',
+        /**
+         * @param {angular.translate.ITranslateProvider} $translateProvider
+         * @param {import('@uirouter/angularjs').StateProvider} $stateProvider
+         * @param {ng.ILocationProvider} $locationProvider
+         * @param {import('@uirouter/angularjs').UrlRouterProvider} $urlRouterProvider
+         */
+        ($translateProvider, $stateProvider, $locationProvider, $urlRouterProvider) => {
+            $translateProvider.translations('en', i18n);
+            $translateProvider.preferredLanguage('en');
+
+            // Set up the states.
+            $stateProvider
+                .state('base', {
+                    url: '',
+                    abstract: true,
+                    template: baseTemplate
+                });
+
+            $urlRouterProvider.otherwise('/404');
+            $locationProvider.html5Mode(true);
+        }])
+    .run(['$rootScope', '$state', 'gettingStarted',
+        /**
+         * @param {ng.IRootScopeService} $root
+         * @param {import('@uirouter/angularjs').StateService} $state
+         * @param {ReturnType<typeof import('./modules/getting-started/GettingStarted.provider').service>} gettingStarted
+         */
+        ($root, $state, gettingStarted) => {
+            $root._ = _;
+            $root.$state = $state;
+            $root.gettingStarted = gettingStarted;
+        }
+    ])
+    .run(['$rootScope', 'AgentManager',
+        /**
+         * @param {ng.IRootScopeService} $root
+         * @param {import('./modules/agent/AgentManager.service').default} agentMgr
+         */
+        ($root, agentMgr) => {
+            let lastUser;
+
+            $root.$on('user', (e, user) => {
+                if (lastUser)
+                    return;
+
+                lastUser = user;
+
+                agentMgr.connect();
+            });
+        }
+    ])
+    .run(['$transitions',
+        /**
+         * @param {import('@uirouter/angularjs').TransitionService} $transitions
+         */
+        ($transitions) => {
+            $transitions.onSuccess({ }, (trans) => {
+                try {
+                    const {name, unsaved} = trans.$to();
+                    const params = trans.params();
+
+                    if (unsaved)
+                        localStorage.removeItem('lastStateChangeSuccess');
+                    else
+                        localStorage.setItem('lastStateChangeSuccess', JSON.stringify({name, params}));
+                }
+                catch (ignored) {
+                    // No-op.
+                }
+            });
+        }
+    ])
+    .run(['$rootScope', '$http', '$state', 'IgniteMessages', 'User', 'IgniteNotebookData',
+        /**
+         * @param {ng.IRootScopeService} $root
+         * @param {ng.IHttpService} $http
+         * @param {ReturnType<typeof import('./services/Messages.service').default>} Messages
+         */
+        ($root, $http, $state, Messages, User, Notebook) => { // eslint-disable-line no-shadow
+            $root.revertIdentity = () => {
+                $http.get('/api/v1/admin/revert/identity')
+                    .then(() => User.load())
+                    .then(() => $state.go('base.settings.admin'))
+                    .then(() => Notebook.load())
+                    .catch(Messages.showError);
+            };
+        }
+    ])
+    .run(['IgniteIcon',
+        /**
+         * @param {import('./components/ignite-icon/service').default} IgniteIcon
+         */
+        (IgniteIcon) => IgniteIcon.registerIcons(icons)
+    ]);
diff --git a/modules/frontend/app/browserUpdate/index.js b/modules/frontend/app/browserUpdate/index.js
new file mode 100644
index 0000000..3930ac7
--- /dev/null
+++ b/modules/frontend/app/browserUpdate/index.js
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import browserUpdate from 'browser-update';
+import './style.scss';
+
+browserUpdate({
+    notify: {
+        i: 11,
+        f: '-18m',
+        s: 9,
+        c: '-18m',
+        o: '-18m',
+        e: '-6m'
+    },
+    l: 'en',
+    mobile: false,
+    api: 5,
+    // This should work in older browsers
+    text: '<b>Outdated or unsupported browser detected.</b> Web Console may work incorrectly. Please update to one of modern fully supported browsers! <a {up_but}>Update</a> <a {ignore_but}>Ignore</a>',
+    reminder: 0
+});
diff --git a/modules/frontend/app/browserUpdate/style.scss b/modules/frontend/app/browserUpdate/style.scss
new file mode 100644
index 0000000..060fb20
--- /dev/null
+++ b/modules/frontend/app/browserUpdate/style.scss
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import "./../primitives/btn/index.scss";
+
+#buorg.buorg {
+    background-color: $brand-warning;
+    line-height: 16px;
+    font-family: inherit;
+
+    a {
+        @extend .btn-ignite;
+
+        &#buorgul {
+            @extend .btn-ignite--success;
+        }
+
+        &#buorgig {
+            @extend .btn-ignite--primary;
+        }
+    }
+}
diff --git a/modules/frontend/app/components/activities-user-dialog/activities-user-dialog.controller.js b/modules/frontend/app/components/activities-user-dialog/activities-user-dialog.controller.js
new file mode 100644
index 0000000..bd3bb07
--- /dev/null
+++ b/modules/frontend/app/components/activities-user-dialog/activities-user-dialog.controller.js
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class ActivitiesCtrl {
+    static $inject = ['user'];
+
+    constructor(user) {
+        const $ctrl = this;
+
+        $ctrl.user = user;
+        $ctrl.data = _.map(user.activitiesDetail, (amount, action) => ({action, amount}));
+
+        $ctrl.columnDefs = [
+            { displayName: 'Description', field: 'action', enableFiltering: false, cellFilter: 'translate', minWidth: 120, width: '43%'},
+            { displayName: 'Action', field: 'action', enableFiltering: false, minWidth: 120, width: '43%'},
+            { displayName: 'Visited', field: 'amount', enableFiltering: false, minWidth: 80}
+        ];
+    }
+}
diff --git a/modules/frontend/app/components/activities-user-dialog/activities-user-dialog.tpl.pug b/modules/frontend/app/components/activities-user-dialog/activities-user-dialog.tpl.pug
new file mode 100644
index 0000000..511a37b
--- /dev/null
+++ b/modules/frontend/app/components/activities-user-dialog/activities-user-dialog.tpl.pug
@@ -0,0 +1,34 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.modal.modal--ignite.modal--wide.theme--ignite(tabindex='-1' role='dialog')
+    .modal-dialog
+        .modal-content
+            .modal-header
+                h4.modal-title Activity details: {{ ctrl.user.userName }}
+                button.close(type='button' aria-label='Close' ng-click='$hide()')
+                     svg(ignite-icon='cross')
+            .modal-body.modal-body-with-scroll
+                .panel--ignite
+                    ignite-grid-table(
+                        items='ctrl.data'
+                        column-defs='ctrl.columnDefs'
+                        grid-thin='true'
+                    )
+
+            .modal-footer
+                div
+                    button.btn-ignite.btn-ignite--success(ng-click='$hide()') Close
diff --git a/modules/frontend/app/components/activities-user-dialog/index.js b/modules/frontend/app/components/activities-user-dialog/index.js
new file mode 100644
index 0000000..ace3821
--- /dev/null
+++ b/modules/frontend/app/components/activities-user-dialog/index.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import controller from './activities-user-dialog.controller';
+import templateUrl from './activities-user-dialog.tpl.pug';
+
+/**
+ * @param {mgcrea.ngStrap.modal.IModalService} $modal
+ */
+export default function service($modal) {
+    return function({ show = true, user }) {
+        const ActivitiesUserDialog = $modal({
+            templateUrl,
+            show,
+            resolve: {
+                user: () => user
+            },
+            controller,
+            controllerAs: 'ctrl'
+        });
+
+        return ActivitiesUserDialog.$promise
+             .then(() => ActivitiesUserDialog);
+    };
+}
+
+service.$inject = ['$modal'];
diff --git a/modules/frontend/app/components/breadcrumbs/component.js b/modules/frontend/app/components/breadcrumbs/component.js
new file mode 100644
index 0000000..5d992fc
--- /dev/null
+++ b/modules/frontend/app/components/breadcrumbs/component.js
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+
+export class Breadcrumbs {
+    static $inject = ['$transclude', '$element'];
+    /**
+     * @param {ng.ITranscludeFunction} $transclude
+     * @param {JQLite} $element
+     */
+    constructor($transclude, $element) {
+        this.$transclude = $transclude;
+        this.$element = $element;
+    }
+    $postLink() {
+        this.$transclude((clone) => {
+            clone.first().prepend(this.$element.find('.breadcrumbs__home'));
+            this.$element.append(clone);
+        });
+    }
+}
+
+export default {
+    controller: Breadcrumbs,
+    template,
+    transclude: true
+};
diff --git a/modules/frontend/app/components/breadcrumbs/index.js b/modules/frontend/app/components/breadcrumbs/index.js
new file mode 100644
index 0000000..72b026d
--- /dev/null
+++ b/modules/frontend/app/components/breadcrumbs/index.js
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('ignite-console.breadcrumbs', [])
+    .component('breadcrumbs', component);
diff --git a/modules/frontend/app/components/breadcrumbs/style.scss b/modules/frontend/app/components/breadcrumbs/style.scss
new file mode 100644
index 0000000..1638ea6
--- /dev/null
+++ b/modules/frontend/app/components/breadcrumbs/style.scss
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+breadcrumbs {
+    $side-margin: 5px;
+
+    padding: 3px $side-margin;
+    display: inline-flex;
+    align-items: center;
+    flex-direction: row;
+
+    min-height: 20px;
+    border-radius: 4px;
+    background-color: rgba(197, 197, 197, 0.1);
+    font-size: 12px;
+    line-height: 1.08;
+    text-align: left;
+    color: #393939;
+
+    .#{&}__home {
+        margin-left: $side-margin;
+        margin-right: $side-margin;
+        vertical-align: -1px;
+        width: 12px;
+        height: 12px;
+    }
+
+    [ui-sref] + [ui-sref]:before {
+        content: '/';
+        margin: 0 $side-margin;
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/components/breadcrumbs/template.pug b/modules/frontend/app/components/breadcrumbs/template.pug
new file mode 100644
index 0000000..dacc1c3
--- /dev/null
+++ b/modules/frontend/app/components/breadcrumbs/template.pug
@@ -0,0 +1,17 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+svg.breadcrumbs__home(ignite-icon='home')
\ No newline at end of file
diff --git a/modules/frontend/app/components/bs-select-menu/controller.js b/modules/frontend/app/components/bs-select-menu/controller.js
new file mode 100644
index 0000000..70346ed
--- /dev/null
+++ b/modules/frontend/app/components/bs-select-menu/controller.js
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class {
+    static $inject = ['$scope'];
+
+    /**
+     * @param {ng.IScope} $scope
+     */
+    constructor($scope) {
+        this.$scope = $scope;
+    }
+
+    areAllSelected() {
+        return this.$scope.$matches.every(({index}) => this.$scope.$isActive(index));
+    }
+}
diff --git a/modules/frontend/app/components/bs-select-menu/directive.js b/modules/frontend/app/components/bs-select-menu/directive.js
new file mode 100644
index 0000000..44aa199
--- /dev/null
+++ b/modules/frontend/app/components/bs-select-menu/directive.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+import './style.scss';
+
+export default function bsSelectMenu() {
+    return {
+        template,
+        controller,
+        controllerAs: '$ctrl',
+        restrict: 'E',
+        replace: true // Workaround: without [replace: true] bs-select detects incorrect menu size.
+    };
+}
diff --git a/modules/frontend/app/components/bs-select-menu/index.js b/modules/frontend/app/components/bs-select-menu/index.js
new file mode 100644
index 0000000..0f2a258
--- /dev/null
+++ b/modules/frontend/app/components/bs-select-menu/index.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import directive from './directive';
+import {default as transcludeToBody} from './transcludeToBody.directive';
+import stripFilter from './strip.filter';
+
+export default angular
+    .module('ignite-console.bs-select-menu', [])
+    .directive('bssmTranscludeToBody', transcludeToBody)
+    .directive('bsSelectMenu', directive)
+    .filter('bsSelectStrip', stripFilter);
diff --git a/modules/frontend/app/components/bs-select-menu/index.spec.js b/modules/frontend/app/components/bs-select-menu/index.spec.js
new file mode 100644
index 0000000..06a292f
--- /dev/null
+++ b/modules/frontend/app/components/bs-select-menu/index.spec.js
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'mocha';
+import {assert} from 'chai';
+import angular from 'angular';
+import componentModule from './index';
+
+suite('bs-select-menu', () => {
+    /** @type {ng.IScope} */
+    let $scope;
+    /** @type {ng.ICompileService} */
+    let $compile;
+
+    setup(() => {
+        angular.module('test', [componentModule.name]);
+        angular.mock.module('test');
+        angular.mock.inject((_$rootScope_, _$compile_) => {
+            $compile = _$compile_;
+            $scope = _$rootScope_.$new();
+        });
+    });
+
+    test('Create/destroy', () => {
+        $scope.$matches = [];
+        $scope.show = false;
+        const el = angular.element(`
+            <div ng-if='show'>
+                <bs-select-menu></bs-select-menu>
+            </div>
+        `);
+
+        const overlay = () => document.body.querySelector('.bssm-click-overlay');
+
+        $compile(el)($scope);
+        $scope.$digest();
+        assert.notOk(overlay(), 'No overlay on init');
+
+        $scope.show = true;
+        $scope.$isShown = true;
+        $scope.$digest();
+        assert.ok(overlay(), 'Adds overlay to body on show');
+
+        $scope.show = false;
+        $scope.$digest();
+        assert.notOk(overlay(), 'Removes overlay when element is removed from DOM');
+
+        $scope.show = true;
+        $scope.$isShown = false;
+        $scope.$digest();
+        assert.notOk(overlay(), 'Removes overlay menu is closed');
+    });
+});
diff --git a/modules/frontend/app/components/bs-select-menu/strip.filter.js b/modules/frontend/app/components/bs-select-menu/strip.filter.js
new file mode 100644
index 0000000..f2a18e4
--- /dev/null
+++ b/modules/frontend/app/components/bs-select-menu/strip.filter.js
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default function() {
+    return function(val) {
+        return val ? val.replace(/(<\/?\w+>)/igm, '') : '';
+    };
+}
diff --git a/modules/frontend/app/components/bs-select-menu/style.scss b/modules/frontend/app/components/bs-select-menu/style.scss
new file mode 100644
index 0000000..4997a6d
--- /dev/null
+++ b/modules/frontend/app/components/bs-select-menu/style.scss
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.bs-select-menu {
+    @import '../../../public/stylesheets/variables.scss';
+
+    $item-height: 30px;
+    $max-visible-items: 11;
+
+    z-index: 2000;
+    padding: 0;
+    margin: 0;
+    list-style: none;
+    position: absolute;
+    outline: none !important;
+    overflow-y: auto;
+    overflow-x: hidden;
+    max-height: $max-visible-items * $item-height;
+    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.3);
+    border-radius: $ignite-button-border-radius;
+    border: 1px solid #c5c5c5;
+    background: white;
+    color: initial; // Fixes color inheritance inside some containers
+
+    .bssm-active-indicator {
+        font-size: 14px;
+        width: 12px;
+        color: #afafaf;
+
+        &.bssm-active-indicator__active {
+            color: $ignite-brand-success;
+        }
+    }
+
+    .bssm-item-text {
+        overflow: visible;
+        white-space: nowrap;
+    }
+
+    &>li {
+        width: 100%;
+
+        &>.bssm-item-button {
+            width: 100%;
+            justify-content: flex-start;
+            border-bottom: 1px solid #dedede;
+            padding-bottom: 9px;
+            background-color: transparent;
+            border-radius: 0;
+            padding-right: 30px;
+
+            &:hover {
+                background-color: #eeeeee;
+            }
+        }
+
+        &:last-of-type > .bssm-item-button {
+            border-bottom: none;
+            padding-bottom: 10px;
+        }
+    }
+
+    [class*='bssm-multiple'] {
+        .bssm-active-indicator {
+            display: initial;
+        }
+    }
+
+    &:not([class*='bssm-multiple']) {
+        .bssm-active-indicator {
+            display: none;
+        }
+
+        & > li > .bssm-item-button__active {
+            background-color: #e5f2f9;
+        }
+    }
+}
+
+.bssm-click-overlay {
+    position: fixed;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 1999;
+}
diff --git a/modules/frontend/app/components/bs-select-menu/template.pug b/modules/frontend/app/components/bs-select-menu/template.pug
new file mode 100644
index 0000000..7cd2195
--- /dev/null
+++ b/modules/frontend/app/components/bs-select-menu/template.pug
@@ -0,0 +1,47 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+ul.bs-select-menu(
+    tabindex='-1'
+    ng-show='$isVisible()'
+    ng-class=`{ 'bssm-multiple': $isMultiple }`
+    role='select'
+)
+    li(ng-if='$showAllNoneButtons || ($isMultiple && $matches.length > 2)')
+        button.btn-ignite.bssm-item-button(
+            ng-click='$ctrl.areAllSelected() ? $selectNone() : $selectAll()'
+            type='button'
+        )
+            img.bssm-active-indicator.icon-left(
+                ng-src='{{ $ctrl.areAllSelected() ? "/images/checkbox-active.svg" : "/images/checkbox.svg" }}'
+            )
+            | All
+    li(role='presentation' ng-repeat='match in $matches')
+        button.btn-ignite.bssm-item-button(
+            type='button'
+            role='menuitem'
+            tabindex='-1'
+            ng-click='$select($index, $event); $event.stopPropagation();'
+            ng-class=`{ 'bssm-item-button__active': $isActive($index) }`
+            data-placement='right auto'
+            title='{{ ::match.label | bsSelectStrip }}'
+        )
+            img.bssm-active-indicator.icon-left(
+                ng-src='{{ $isActive($index) ? "/images/checkbox-active.svg" : "/images/checkbox.svg" }}'
+            )
+            span.bssm-item-text(ng-bind-html='match.label')
+    bssm-transclude-to-body(ng-if='$isShown')
+        .bssm-click-overlay(ng-click='$hide()')
diff --git a/modules/frontend/app/components/bs-select-menu/transcludeToBody.directive.js b/modules/frontend/app/components/bs-select-menu/transcludeToBody.directive.js
new file mode 100644
index 0000000..de4fbfa
--- /dev/null
+++ b/modules/frontend/app/components/bs-select-menu/transcludeToBody.directive.js
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class Controller {
+    static $inject = ['$transclude', '$document'];
+
+    /**
+     * @param {ng.ITranscludeFunction} $transclude
+     * @param {JQLite} $document
+     */
+    constructor($transclude, $document) {
+        this.$transclude = $transclude;
+        this.$document = $document;
+    }
+
+    $postLink() {
+        this.$transclude((clone) => {
+            this.clone = clone;
+            this.$document.find('body').append(clone);
+        });
+    }
+
+    $onDestroy() {
+        this.clone.remove();
+        this.clone = this.$document = null;
+    }
+}
+
+export default function directive() {
+    return {
+        restrict: 'E',
+        transclude: true,
+        controller: Controller,
+        scope: {}
+    };
+}
diff --git a/modules/frontend/app/components/cluster-security-icon/component.js b/modules/frontend/app/components/cluster-security-icon/component.js
new file mode 100644
index 0000000..abbafe3
--- /dev/null
+++ b/modules/frontend/app/components/cluster-security-icon/component.js
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+
+export default {
+    bindings: {
+        secured: '<'
+    },
+    template
+};
diff --git a/modules/frontend/app/components/cluster-security-icon/index.js b/modules/frontend/app/components/cluster-security-icon/index.js
new file mode 100644
index 0000000..a7e9139
--- /dev/null
+++ b/modules/frontend/app/components/cluster-security-icon/index.js
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import component from './component';
+
+export default angular
+    .module('ignite-console.cluster-security-icon', [])
+    .component('clusterSecurityIcon', component);
diff --git a/modules/frontend/app/components/cluster-security-icon/template.pug b/modules/frontend/app/components/cluster-security-icon/template.pug
new file mode 100644
index 0000000..a4d8f8a
--- /dev/null
+++ b/modules/frontend/app/components/cluster-security-icon/template.pug
@@ -0,0 +1,30 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+svg(
+    ng-if='$ctrl.secured'
+    ignite-icon='lockClosed'
+    bs-tooltip=''
+    data-title='Security Cluster'
+    data-placement='top'
+)
+svg(
+    ng-if='!$ctrl.secured'
+    ignite-icon='lockOpened'
+    bs-tooltip=''
+    data-title='Non Security Cluster'
+    data-placement='top'
+)
diff --git a/modules/frontend/app/components/cluster-selector/component.js b/modules/frontend/app/components/cluster-selector/component.js
new file mode 100644
index 0000000..f6141d9
--- /dev/null
+++ b/modules/frontend/app/components/cluster-selector/component.js
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+import './style.scss';
+
+export default {
+    template,
+    controller
+};
diff --git a/modules/frontend/app/components/cluster-selector/controller.js b/modules/frontend/app/components/cluster-selector/controller.js
new file mode 100644
index 0000000..28ccf27
--- /dev/null
+++ b/modules/frontend/app/components/cluster-selector/controller.js
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+import {BehaviorSubject} from 'rxjs';
+import {combineLatest, filter, tap} from 'rxjs/operators';
+import {CancellationError} from 'app/errors/CancellationError';
+
+export default class {
+    static $inject = ['AgentManager', 'IgniteConfirm', 'IgniteVersion', 'IgniteMessages'];
+
+    /**
+     * @param agentMgr Agent manager.
+     * @param Confirm Confirmation service.
+     * @param Version Version check service.
+     * @param Messages Messages service.
+     */
+    constructor(agentMgr, Confirm, Version, Messages) {
+        this.agentMgr = agentMgr;
+        this.Confirm = Confirm;
+        this.Version = Version;
+        this.Messages = Messages;
+
+        this.clusters = [];
+        this.isDemo = agentMgr.isDemoMode();
+        this._inProgressSubject = new BehaviorSubject(false);
+    }
+
+    $onInit() {
+        if (this.isDemo)
+            return;
+
+        this.inProgress$ = this._inProgressSubject.asObservable();
+
+        this.clusters$ = this.agentMgr.connectionSbj.pipe(
+            combineLatest(this.inProgress$),
+            tap(([sbj, inProgress]) => this.inProgress = inProgress),
+            filter(([sbj, inProgress]) => !inProgress),
+            tap(([{cluster, clusters}]) => {
+                this.cluster = cluster ? {...cluster} : null;
+                this.clusters = _.orderBy(clusters, ['name'], ['asc']);
+            })
+        )
+        .subscribe(() => {});
+    }
+
+    $onDestroy() {
+        if (!this.isDemo)
+            this.clusters$.unsubscribe();
+    }
+
+    change(item) {
+        this.agentMgr.switchCluster(item)
+            .then(() => this.cluster = item)
+            .catch((err) => {
+                if (!(err instanceof CancellationError))
+                    this.Messages.showError('Failed to switch cluster: ', err);
+            });
+    }
+
+    isChangeStateAvailable() {
+        return !this.isDemo && this.cluster && this.Version.since(this.cluster.clusterVersion, '2.0.0');
+    }
+
+    toggle($event) {
+        $event.preventDefault();
+
+        const toggleClusterState = () => {
+            this._inProgressSubject.next(true);
+
+            return this.agentMgr.toggleClusterState()
+                .then(() => this._inProgressSubject.next(false))
+                .catch((err) => {
+                    this._inProgressSubject.next(false);
+
+                    this.Messages.showError('Failed to toggle cluster state: ', err);
+                });
+        };
+
+        if (this.cluster.active) {
+            return this.Confirm.confirm('Are you sure you want to deactivate cluster?')
+                .then(() => toggleClusterState());
+        }
+
+        return toggleClusterState();
+    }
+}
diff --git a/modules/frontend/app/components/cluster-selector/index.js b/modules/frontend/app/components/cluster-selector/index.js
new file mode 100644
index 0000000..2bdbe44
--- /dev/null
+++ b/modules/frontend/app/components/cluster-selector/index.js
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('ignite-console.cluster-selector', [])
+    .component('clusterSelector', component);
diff --git a/modules/frontend/app/components/cluster-selector/style.scss b/modules/frontend/app/components/cluster-selector/style.scss
new file mode 100644
index 0000000..a143d91
--- /dev/null
+++ b/modules/frontend/app/components/cluster-selector/style.scss
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+cluster-selector {
+    @import "./../../../public/stylesheets/variables.scss";
+
+    position: relative;
+    top: 2px;
+
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+	.btn-ignite.btn-ignite--primary,
+    .btn-ignite.btn-ignite--success {
+        border-radius: 9px;
+        min-height: 0;
+
+        color: white;
+        font-size: 12px;
+        font-weight: bold;
+        line-height: 17px;
+        padding-top: 0;
+        padding-bottom: 0;
+
+        button {
+            font-weight: normal;
+            margin: 0 !important;
+        }
+    }
+
+    .cluster-selector--state {
+        width: 85px;
+    }
+
+    div {
+        margin: 0 10px 0 20px;
+        font-size: 12px;
+    }
+
+    div:last-child {
+        margin-left: 10px;
+        color: #EE2B27;
+    }
+
+    [ignite-icon='info'] {
+        margin-left: 7px;
+        color: $ignite-brand-success;
+    }
+
+    [ignite-icon='lockClosed'], [ignite-icon='lockOpened'] {
+        margin-right: 5px;
+        height: 10px;
+        width: 8px;
+    }
+
+    .bs-select-menu {
+        color: $text-color;
+    }
+}
diff --git a/modules/frontend/app/components/cluster-selector/template.pug b/modules/frontend/app/components/cluster-selector/template.pug
new file mode 100644
index 0000000..8f34ca6
--- /dev/null
+++ b/modules/frontend/app/components/cluster-selector/template.pug
@@ -0,0 +1,82 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+button.btn-ignite.btn-ignite--success(
+    data-ng-if='$ctrl.isDemo'
+)
+    | Demo cluster
+
+button.btn-ignite.btn-ignite--primary(
+    data-ng-if='!$ctrl.isDemo && $ctrl.clusters.length == 0'
+)
+    | No clusters available
+
+button.btn-ignite.btn-ignite--primary(
+    data-ng-if='!$ctrl.isDemo && $ctrl.clusters.length == 1'
+)
+    cluster-security-icon(secured='$ctrl.cluster.secured')
+    | {{ $ctrl.cluster.name }}
+
+span(data-ng-if='!$ctrl.isDemo && $ctrl.clusters.length > 1')
+    div.btn-ignite.btn-ignite--primary(
+        bs-dropdown=''
+        data-trigger='click'
+        data-container='body'
+
+        tabindex='0'
+        aria-haspopup='true'
+        aria-expanded='false'
+    )
+        span(ng-if='!$ctrl.cluster') No clusters available
+
+        span(ng-if='$ctrl.cluster')
+            cluster-security-icon(secured='$ctrl.cluster.secured')
+            | {{ $ctrl.cluster.name }}
+            span.icon-right.fa.fa-caret-down
+
+    ul.bs-select-menu.dropdown-menu(role='menu')
+        li(ng-repeat='item in $ctrl.clusters')
+            button.btn-ignite.bssm-item-button(ng-click='$ctrl.change(item)')
+                span.icon-left
+                    svg(ignite-icon='{{ item.secured ? "lockClosed" : "lockOpened" }}')
+                | {{ item.name }}
+
+svg(
+    ng-if='!$ctrl.isDemo'
+    ignite-icon='info'
+    bs-tooltip=''
+    data-title='Multi-Cluster Support<br/>\
+        <a href="https://apacheignite-tools.readme.io/docs/multi-cluster-support" target="_blank">More info</a>'
+    data-placement='bottom'
+)
+
+.cluster-selector--state(ng-if='$ctrl.isChangeStateAvailable()')
+    | Cluster {{ $ctrl.cluster.active ? 'active' : 'inactive' }}
+
++switcher()(
+    ng-if='$ctrl.isChangeStateAvailable()'
+    ng-click='$ctrl.toggle($event)'
+    ng-checked='$ctrl.cluster.active'
+    ng-disabled='$ctrl.inProgress'
+
+    tip='Toggle cluster active state'
+    is-in-progress='{{ $ctrl.inProgress }}'
+)
+
+div(ng-if='$ctrl.inProgress && $ctrl.isChangeStateAvailable()')
+    | {{ !$ctrl.cluster.active ? 'Activating...' : 'Deactivating...' }}
diff --git a/modules/frontend/app/components/connected-clusters-badge/controller.js b/modules/frontend/app/components/connected-clusters-badge/controller.js
new file mode 100644
index 0000000..896c02b
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-badge/controller.js
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {tap} from 'rxjs/operators';
+
+export default class {
+    static $inject = ['$scope', 'AgentManager', 'ConnectedClustersDialog'];
+
+    connectedClusters = 0;
+
+    /**
+     * @param $scope Angular scope.
+     * @param {import('app/modules/agent/AgentManager.service').default} agentMgr
+     * @param {import('../connected-clusters-dialog/service').default} connectedClustersDialog
+     */
+    constructor($scope, agentMgr, connectedClustersDialog) {
+        this.$scope = $scope;
+        this.agentMgr = agentMgr;
+        this.connectedClustersDialog = connectedClustersDialog;
+    }
+
+    show() {
+        this.connectedClustersDialog.show({
+            clusters: this.clusters
+        });
+    }
+
+    $onInit() {
+        this.connectedClusters$ = this.agentMgr.connectionSbj.pipe(
+            tap(({ clusters }) => this.connectedClusters = clusters.length),
+            tap(({ clusters }) => {
+                this.clusters = clusters;
+                this.$scope.$applyAsync();
+            })
+        )
+        .subscribe();
+    }
+
+    $onDestroy() {
+        this.connectedClusters$.unsubscribe();
+    }
+}
diff --git a/modules/frontend/app/components/connected-clusters-badge/index.js b/modules/frontend/app/components/connected-clusters-badge/index.js
new file mode 100644
index 0000000..8d5aa07
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-badge/index.js
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import './style.scss';
+import template from './template.pug';
+import controller from './controller';
+
+import connectedClustersDialog from '../connected-clusters-dialog';
+
+export default angular
+    .module('ignite-console.connected-clusters-badge', [
+        connectedClustersDialog.name
+    ])
+    .component('connectedClusters', {
+        template,
+        controller
+    });
diff --git a/modules/frontend/app/components/connected-clusters-badge/style.scss b/modules/frontend/app/components/connected-clusters-badge/style.scss
new file mode 100644
index 0000000..e46e382
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-badge/style.scss
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+connected-clusters {
+    @import "./../../../public/stylesheets/variables.scss";
+
+    height: 100%;
+    display: flex;
+
+    .connected-cluster-badge__count {
+        color: $ignite-brand-success;
+        margin-left: 5px;
+    }
+
+    button {
+        height: 100%;
+        width: 100%;
+        background: none !important;
+        border: none !important;
+        margin: 0 !important;
+        padding: 0 !important;
+        outline: none !important;
+
+        display: flex;
+        align-items: center;
+
+        font-size: 14px;
+    }
+}
diff --git a/modules/frontend/app/components/connected-clusters-badge/template.pug b/modules/frontend/app/components/connected-clusters-badge/template.pug
new file mode 100644
index 0000000..dfb4bd9
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-badge/template.pug
@@ -0,0 +1,18 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+button(type='button' ng-click='$ctrl.show()')
+    | My Connected Clusters: <span class='connected-cluster-badge__count'>{{ $ctrl.connectedClusters }}</span>
diff --git a/modules/frontend/app/components/connected-clusters-dialog/components/cell-logout/index.js b/modules/frontend/app/components/connected-clusters-dialog/components/cell-logout/index.js
new file mode 100644
index 0000000..e5bdd2c
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-dialog/components/cell-logout/index.js
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+
+class controller {
+    /** @type {string} */
+    clusterId;
+
+    static $inject = ['AgentManager'];
+
+    /**
+     * @param {import('app/modules/agent/AgentManager.service').default} agentMgr
+     */
+    constructor(agentMgr) {
+        this.agentMgr = agentMgr;
+    }
+
+    logout() {
+        this.agentMgr.clustersSecrets.reset(this.clusterId);
+    }
+}
+
+export default {
+    controller,
+    template,
+    bindings: {
+        clusterId: '<'
+    }
+};
diff --git a/modules/frontend/app/components/connected-clusters-dialog/components/cell-logout/template.pug b/modules/frontend/app/components/connected-clusters-dialog/components/cell-logout/template.pug
new file mode 100644
index 0000000..1f29439
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-dialog/components/cell-logout/template.pug
@@ -0,0 +1,23 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+button.btn-ignite.btn-ignite--link-dashed-secondary(
+    ng-click='$event.stopPropagation(); $ctrl.logout()'
+    bs-tooltip=''
+    data-title='Click here to logout from cluster.'
+    data-placement='top'
+)
+    svg(ignite-icon='exit')
\ No newline at end of file
diff --git a/modules/frontend/app/components/connected-clusters-dialog/components/cell-status/index.ts b/modules/frontend/app/components/connected-clusters-dialog/components/cell-status/index.ts
new file mode 100644
index 0000000..c3f201f
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-dialog/components/cell-status/index.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {componentFactory, StatusLevel} from 'app/components/status-output';
+
+export default componentFactory([
+    {
+        level: StatusLevel.GREEN,
+        value: true,
+        label: 'Active'
+    },
+    {
+        level: StatusLevel.RED,
+        value: false,
+        label: 'Not Active'
+    }
+]);
diff --git a/modules/frontend/app/components/connected-clusters-dialog/components/list/column-defs.js b/modules/frontend/app/components/connected-clusters-dialog/components/list/column-defs.js
new file mode 100644
index 0000000..6b25878
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-dialog/components/list/column-defs.js
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default [
+    {
+        name: 'name',
+        displayName: 'Cluster',
+        field: 'name',
+        cellTemplate: `
+            <div class='ui-grid-cell-contents'>
+                <cluster-security-icon
+                    secured='row.entity.secured'
+                ></cluster-security-icon>
+                {{ COL_FIELD }}
+            </div>
+        `,
+        width: 240,
+        minWidth: 240
+    },
+    {
+        name: 'nids',
+        displayName: 'Number of Nodes',
+        field: 'nids.length',
+        cellClass: 'ui-grid-number-cell',
+        width: 160,
+        minWidth: 160
+    },
+    {
+        name: 'status',
+        displayName: 'Status',
+        field: 'active',
+        cellTemplate: `
+            <div class='ui-grid-cell-contents ui-grid-cell--status'>
+                <connected-clusters-cell-status
+                    value='COL_FIELD'
+                ></connected-clusters-cell-status>
+                <connected-clusters-cell-logout
+                    ng-if='row.entity.secured && grid.appScope.$ctrl.agentMgr.hasCredentials(row.entity.id)'
+                    cluster-id='row.entity.id'
+                ></connected-clusters-cell-logout>
+            </div>
+        `,
+        minWidth: 140
+    }
+];
diff --git a/modules/frontend/app/components/connected-clusters-dialog/components/list/controller.js b/modules/frontend/app/components/connected-clusters-dialog/components/list/controller.js
new file mode 100644
index 0000000..b69f8a2
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-dialog/components/list/controller.js
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+import columnDefs from './column-defs';
+
+export default class ConnectedClustersListCtrl {
+    static $inject = ['$scope', 'AgentManager'];
+
+    /**
+     * @param {ng.IScope} $scope
+     * @param {import('app/modules/agent/AgentManager.service').default} agentMgr
+     */
+    constructor($scope, agentMgr) {
+        this.$scope = $scope;
+        this.agentMgr = agentMgr;
+    }
+
+    $onInit() {
+        this.gridOptions = {
+            data: _.orderBy(this.data, ['name'], ['asc']),
+            columnDefs,
+            columnVirtualizationThreshold: 30,
+            rowHeight: 46,
+            enableColumnMenus: false,
+            enableFullRowSelection: true,
+            enableFiltering: false,
+            selectionRowHeaderWidth: 52,
+            onRegisterApi: (api) => {
+                this.gridApi = api;
+
+                this.$scope.$watch(() => this.gridApi.grid.getVisibleRows().length, (rows) => this.adjustHeight(rows));
+            }
+        };
+    }
+
+    adjustHeight(rows) {
+        // One row for grid-no-data.
+        const height = Math.max(1, Math.min(rows, 6)) * 48 + 49 + (rows ? 8 : 0);
+
+        this.gridApi.grid.element.css('height', height + 'px');
+
+        this.gridApi.core.handleWindowResize();
+    }
+}
diff --git a/modules/frontend/app/components/connected-clusters-dialog/components/list/index.js b/modules/frontend/app/components/connected-clusters-dialog/components/list/index.js
new file mode 100644
index 0000000..6bcb3e7
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-dialog/components/list/index.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './style.scss';
+import templateUrl from './template.tpl.pug';
+import controller from './controller';
+
+export default {
+    controller,
+    templateUrl,
+    bindings: {
+        gridApi: '=?',
+        data: '<?options'
+    }
+};
diff --git a/modules/frontend/app/components/connected-clusters-dialog/components/list/style.scss b/modules/frontend/app/components/connected-clusters-dialog/components/list/style.scss
new file mode 100644
index 0000000..180f65e
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-dialog/components/list/style.scss
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+connected-clusters-list {
+    position: relative;
+    display: block;
+
+    .ui-grid-cell--status {
+        display: flex;
+        justify-content: space-between;
+    }
+
+    .ui-grid-header-cell:last-child .ui-grid-column-resizer.right {
+        border-right: none;
+    }
+
+    .ui-grid-viewport {
+        overflow: auto !important;
+    }
+
+    connected-clusters-cell-logout {
+        margin-top: -8px;
+        margin-right: -10px;
+    }
+
+    grid-no-data {
+        position: absolute;
+        top: 50%;
+
+        width: 100%;
+        padding: 0 20px;
+
+        text-align: center;
+    }
+}
diff --git a/modules/frontend/app/components/connected-clusters-dialog/components/list/template.tpl.pug b/modules/frontend/app/components/connected-clusters-dialog/components/list/template.tpl.pug
new file mode 100644
index 0000000..db7c259
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-dialog/components/list/template.tpl.pug
@@ -0,0 +1,19 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.grid.ui-grid--ignite(ui-grid='$ctrl.gridOptions' ui-grid-resize-columns ui-grid-hovering ui-grid-filters)
+grid-no-data(grid-api='$ctrl.gridApi')
+    | You have no connected clusters.
diff --git a/modules/frontend/app/components/connected-clusters-dialog/controller.js b/modules/frontend/app/components/connected-clusters-dialog/controller.js
new file mode 100644
index 0000000..79975db
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-dialog/controller.js
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class {
+    static $inject = ['clusters'];
+
+    constructor(clusters) {
+        this.clusters = clusters;
+    }
+}
diff --git a/modules/frontend/app/components/connected-clusters-dialog/index.js b/modules/frontend/app/components/connected-clusters-dialog/index.js
new file mode 100644
index 0000000..f4abf27
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-dialog/index.js
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import clusterSecurityIcon from '../cluster-security-icon';
+
+import ConnectedClustersDialog from './service';
+
+import connectedClustersList from './components/list';
+import connectedClustersCellStatus from './components/cell-status';
+import connectedClustersCellLogout from './components/cell-logout';
+
+export default angular
+    .module('ignite-console.connected-clusters-dialog', [
+        clusterSecurityIcon.name
+    ])
+    .service('ConnectedClustersDialog', ConnectedClustersDialog)
+    .component('connectedClustersList', connectedClustersList)
+    .component('connectedClustersCellStatus', connectedClustersCellStatus)
+    .component('connectedClustersCellLogout', connectedClustersCellLogout);
diff --git a/modules/frontend/app/components/connected-clusters-dialog/service.js b/modules/frontend/app/components/connected-clusters-dialog/service.js
new file mode 100644
index 0000000..243c330
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-dialog/service.js
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './style.scss';
+import controller from './controller';
+import templateUrl from './template.tpl.pug';
+
+export default class {
+    static $inject = ['$modal'];
+
+    /**
+     * @param {mgcrea.ngStrap.modal.IModalService} $modal
+     */
+    constructor($modal) {
+        this.$modal = $modal;
+    }
+
+    show({ clusters }) {
+        const modal = this.$modal({
+            templateUrl,
+            resolve: {
+                clusters: () => clusters
+            },
+            controller,
+            controllerAs: '$ctrl'
+        });
+
+        return modal.$promise;
+    }
+}
diff --git a/modules/frontend/app/components/connected-clusters-dialog/style.scss b/modules/frontend/app/components/connected-clusters-dialog/style.scss
new file mode 100644
index 0000000..4f9dd87
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-dialog/style.scss
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.connected-clusters-dialog {
+    .btn-ignite.btn-ignite--success {
+        padding-left: 23px;
+        padding-right: 23px;
+    }
+}
diff --git a/modules/frontend/app/components/connected-clusters-dialog/template.tpl.pug b/modules/frontend/app/components/connected-clusters-dialog/template.tpl.pug
new file mode 100644
index 0000000..5ea83fd
--- /dev/null
+++ b/modules/frontend/app/components/connected-clusters-dialog/template.tpl.pug
@@ -0,0 +1,34 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+.modal.modal--ignite.theme-ignite.connected-clusters-dialog(tabindex='-1' role='dialog')
+    .modal-dialog.modal-dialog--adjust-height
+        form.modal-content(name='$ctrl.form' novalidate)
+            .modal-header
+                h4.modal-title
+                    | Connected Clusters
+                button.close(type='button' aria-label='Close' ng-click='$hide()')
+                     svg(ignite-icon="cross")
+            .modal-body.modal-body-with-scroll
+                .panel--ignite
+                    connected-clusters-list(data-options='$ctrl.clusters')
+
+            .modal-footer
+                div
+                    button.btn-ignite.btn-ignite--success(type='button' ng-click='$hide()') Ok
+
diff --git a/modules/frontend/app/components/dialog-admin-create-user/component.ts b/modules/frontend/app/components/dialog-admin-create-user/component.ts
new file mode 100644
index 0000000..3a60d93
--- /dev/null
+++ b/modules/frontend/app/components/dialog-admin-create-user/component.ts
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import {DialogAdminCreateUser} from './controller';
+
+export default {
+    template,
+    controller: DialogAdminCreateUser,
+    bindings: {
+        close: '&onHide'
+    }
+};
diff --git a/modules/frontend/app/components/dialog-admin-create-user/controller.ts b/modules/frontend/app/components/dialog-admin-create-user/controller.ts
new file mode 100644
index 0000000..e8a29e1
--- /dev/null
+++ b/modules/frontend/app/components/dialog-admin-create-user/controller.ts
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Auth from '../../modules/user/Auth.service';
+import MessagesFactory from '../../services/Messages.service';
+import FormUtilsFactoryFactory from '../../services/FormUtils.service';
+import LoadingServiceFactory from '../../modules/loading/loading.service';
+import {ISignupData} from '../form-signup';
+
+export class DialogAdminCreateUser {
+    close: ng.ICompiledExpression;
+
+    form: ng.IFormController;
+
+    data: ISignupData = {
+        email: null,
+        password: null,
+        firstName: null,
+        lastName: null,
+        company: null,
+        country: null
+    };
+
+    serverError: string | null = null;
+
+    static $inject = ['Auth', 'IgniteMessages', 'IgniteFormUtils', 'IgniteLoading'];
+
+    constructor(
+        private Auth: Auth,
+        private IgniteMessages: ReturnType<typeof MessagesFactory>,
+        private IgniteFormUtils: ReturnType<typeof FormUtilsFactoryFactory>,
+        private loading: ReturnType<typeof LoadingServiceFactory>
+    ) {}
+
+    canSubmitForm(form: DialogAdminCreateUser['form']) {
+        return form.$error.server ? true : !form.$invalid;
+    }
+
+    setServerError(error: DialogAdminCreateUser['serverError']) {
+        this.serverError = error;
+    }
+
+    createUser() {
+        this.IgniteFormUtils.triggerValidation(this.form);
+
+        this.setServerError(null);
+
+        if (!this.canSubmitForm(this.form))
+            return;
+
+        this.loading.start('createUser');
+
+        this.Auth.signup(this.data, false)
+            .then(() => {
+                this.IgniteMessages.showInfo(`User ${this.data.email} created`);
+                this.close({});
+            })
+            .catch((res) => {
+                this.loading.finish('createUser');
+                this.IgniteMessages.showError(null, res.data);
+                this.setServerError(res.data);
+            });
+    }
+}
diff --git a/modules/frontend/app/components/dialog-admin-create-user/index.ts b/modules/frontend/app/components/dialog-admin-create-user/index.ts
new file mode 100644
index 0000000..5a23368
--- /dev/null
+++ b/modules/frontend/app/components/dialog-admin-create-user/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import component from './component';
+import {registerState} from './state';
+
+export default angular.module('ignite-console.dialog-admin-create-user', [])
+    .run(registerState)
+    .component('dialogAdminCreateUser', component);
diff --git a/modules/frontend/app/components/dialog-admin-create-user/state.ts b/modules/frontend/app/components/dialog-admin-create-user/state.ts
new file mode 100644
index 0000000..c64238e
--- /dev/null
+++ b/modules/frontend/app/components/dialog-admin-create-user/state.ts
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {UIRouter} from '@uirouter/angularjs';
+import {dialogState} from '../../utils/dialogState';
+
+registerState.$inject = ['$uiRouter'];
+
+export function registerState(router: UIRouter) {
+    router.stateRegistry.register({
+        ...dialogState('dialog-admin-create-user'),
+        name: 'base.settings.admin.createUser',
+        url: '/create-user'
+    });
+}
diff --git a/modules/frontend/app/components/dialog-admin-create-user/template.pug b/modules/frontend/app/components/dialog-admin-create-user/template.pug
new file mode 100644
index 0000000..0a9f2b4
--- /dev/null
+++ b/modules/frontend/app/components/dialog-admin-create-user/template.pug
@@ -0,0 +1,37 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.modal-dialog(ng-click='$event.stopPropagation()' )
+    form.modal-content(
+        name='$ctrl.form' novalidate
+        ignite-loading='createUser'
+        ignite-loading-text='Creating user…'
+    )
+        .modal-header
+            h4.modal-title Create User
+
+            button.close(type='button' aria-label='Close' ng-click='$ctrl.close()')
+                svg(ignite-icon="cross")
+        .modal-body
+            form-signup(
+                outer-form='$ctrl.form'
+                ng-model='$ctrl.data'
+                server-error='$ctrl.serverError'
+            )
+        .modal-footer
+            div
+                button.btn-ignite.btn-ignite--link-success(ng-click='$ctrl.close()') Cancel
+                button.btn-ignite.btn-ignite--success(ng-click='$ctrl.createUser()') Create user
diff --git a/modules/frontend/app/components/form-field/components/form-field-size/controller.ts b/modules/frontend/app/components/form-field/components/form-field-size/controller.ts
new file mode 100644
index 0000000..fa36507
--- /dev/null
+++ b/modules/frontend/app/components/form-field/components/form-field-size/controller.ts
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import get from 'lodash/get';
+import {IInputErrorNotifier} from '../../../../types';
+
+interface ISizeTypeOption {
+    label: string,
+    value: number
+}
+
+type ISizeType = Array<ISizeTypeOption>;
+
+interface ISizeTypes {
+    [name: string]: ISizeType
+}
+
+export default class PCFormFieldSizeController<T> implements IInputErrorNotifier {
+    ngModel: ng.INgModelController;
+    min?: number;
+    max?: number;
+    onScaleChange: ng.ICompiledExpression;
+    innerForm: ng.IFormController;
+    autofocus?: boolean;
+    id = Math.random();
+    inputElement?: HTMLInputElement;
+    sizesMenu?: Array<ISizeTypeOption>;
+    private _sizeScale: ISizeTypeOption;
+    value: number;
+
+    static $inject = ['$element', '$attrs'];
+
+    static sizeTypes: ISizeTypes = {
+        bytes: [
+            {label: 'Kb', value: 1024},
+            {label: 'Mb', value: 1024 * 1024},
+            {label: 'Gb', value: 1024 * 1024 * 1024}
+        ],
+        gigabytes: [
+            {label: 'Gb', value: 1},
+            {label: 'Tb', value: 1024}
+        ],
+        seconds: [
+            {label: 'ns', value: 1 / 1000},
+            {label: 'ms', value: 1},
+            {label: 's', value: 1000}
+        ],
+        time: [
+            {label: 'sec', value: 1},
+            {label: 'min', value: 60},
+            {label: 'hour', value: 60 * 60}
+        ]
+    };
+
+    constructor(private $element: JQLite, private $attrs: ng.IAttributes) {}
+
+    $onDestroy() {
+        delete this.$element[0].focus;
+        this.$element = this.inputElement = null;
+    }
+
+    $onInit() {
+        if (!this.min) this.min = 0;
+        if (!this.sizesMenu) this.setDefaultSizeType();
+        this.$element.addClass('form-field');
+        this.ngModel.$render = () => this.assignValue(this.ngModel.$viewValue);
+    }
+
+    $postLink() {
+        if ('min' in this.$attrs)
+            this.ngModel.$validators.min = (value) => this.ngModel.$isEmpty(value) || value === void 0 || value >= this.min;
+        if ('max' in this.$attrs)
+            this.ngModel.$validators.max = (value) => this.ngModel.$isEmpty(value) || value === void 0 || value <= this.max;
+
+        this.ngModel.$validators.step = (value) => this.ngModel.$isEmpty(value) || value === void 0 || Math.floor(value) === value;
+        this.inputElement = this.$element[0].querySelector('input');
+        this.$element[0].focus = () => this.inputElement.focus();
+    }
+
+    $onChanges(changes) {
+        if ('sizeType' in changes) {
+            this.sizesMenu = PCFormFieldSizeController.sizeTypes[changes.sizeType.currentValue];
+            this.sizeScale = this.chooseSizeScale(get(changes, 'sizeScaleLabel.currentValue'));
+        }
+        if (!this.sizesMenu) this.setDefaultSizeType();
+        if ('sizeScaleLabel' in changes)
+            this.sizeScale = this.chooseSizeScale(changes.sizeScaleLabel.currentValue);
+
+        if ('min' in changes) this.ngModel.$validate();
+    }
+
+    set sizeScale(value: ISizeTypeOption) {
+        this._sizeScale = value;
+        if (this.onScaleChange) this.onScaleChange({$event: this.sizeScale});
+        if (this.ngModel) this.assignValue(this.ngModel.$viewValue);
+    }
+
+    get sizeScale() {
+        return this._sizeScale;
+    }
+
+    assignValue(rawValue: number) {
+        if (!this.sizesMenu) this.setDefaultSizeType();
+        return this.value = rawValue
+            ? rawValue / this.sizeScale.value
+            : rawValue;
+    }
+
+    onValueChange() {
+        this.ngModel.$setViewValue(this.value ? this.value * this.sizeScale.value : this.value);
+    }
+
+    _defaultLabel() {
+        if (!this.sizesMenu)
+            return;
+
+        return this.sizesMenu[1].label;
+    }
+
+    chooseSizeScale(label = this._defaultLabel()) {
+        if (!label)
+            return;
+
+        return this.sizesMenu.find((option) => option.label.toLowerCase() === label.toLowerCase());
+    }
+
+    setDefaultSizeType() {
+        this.sizesMenu = PCFormFieldSizeController.sizeTypes.bytes;
+        this.sizeScale = this.chooseSizeScale();
+    }
+
+    isTooltipValidation(): boolean {
+        return !this.$element.parents('.theme--ignite-errors-horizontal').length;
+    }
+
+    notifyAboutError() {
+        if (this.$element && this.isTooltipValidation())
+            this.$element.find('.form-field__error [bs-tooltip]').trigger('mouseenter');
+    }
+
+    hideError() {
+        if (this.$element && this.isTooltipValidation())
+            this.$element.find('.form-field__error [bs-tooltip]').trigger('mouseleave');
+    }
+
+    triggerBlur() {
+        this.$element[0].dispatchEvent(new FocusEvent('blur', {relatedTarget: this.inputElement}));
+    }
+}
diff --git a/modules/frontend/app/components/form-field/components/form-field-size/index.js b/modules/frontend/app/components/form-field/components/form-field-size/index.js
new file mode 100644
index 0000000..e7da7d5
--- /dev/null
+++ b/modules/frontend/app/components/form-field/components/form-field-size/index.js
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './style.scss';
+import template from './template.pug';
+import controller from './controller';
+
+export default {
+    controller,
+    template,
+    transclude: true,
+    require: {
+        ngModel: 'ngModel'
+    },
+    bindings: {
+        label: '@',
+        placeholder: '@',
+        min: '@?',
+        max: '@?',
+        tip: '@',
+        required: '<?',
+        sizeType: '@?',
+        sizeScaleLabel: '@?',
+        onScaleChange: '&?',
+        ngDisabled: '<?',
+        autofocus: '<?'
+    }
+};
diff --git a/modules/frontend/app/components/form-field/components/form-field-size/style.scss b/modules/frontend/app/components/form-field/components/form-field-size/style.scss
new file mode 100644
index 0000000..8b86e05
--- /dev/null
+++ b/modules/frontend/app/components/form-field/components/form-field-size/style.scss
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+form-field-size {
+    --scale-select-width: 60px;
+    --error-area-width: 36px;
+    --default-input-padding-horizontal: 10px;
+
+	display: block;
+
+    .form-field__error {
+        width: var(--error-area-width);
+        position: absolute;
+        right: var(--scale-select-width);
+        bottom: 0;
+    }
+
+    &.ng-invalid input, input.ng-invalid {
+        padding-right: var(--error-area-width);
+    }
+}
diff --git a/modules/frontend/app/components/form-field/components/form-field-size/template.pug b/modules/frontend/app/components/form-field/components/form-field-size/template.pug
new file mode 100644
index 0000000..6a2974d
--- /dev/null
+++ b/modules/frontend/app/components/form-field/components/form-field-size/template.pug
@@ -0,0 +1,79 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
++form-field__label({
+    label: '{{ ::$ctrl.label }}',
+    name: '$ctrl.id',
+    required: '$ctrl.required',
+    disabled: '$ctrl.ngDisabled'
+})
+    +form-field__tooltip({title: '{{$ctrl.tip}}'})(
+        ng-if='$ctrl.tip'
+    )
+
+.form-field__control.form-field__control-group(ng-form='$ctrl.innerForm')
+    input(
+        type='number'
+        id='{{::$ctrl.id}}Input'
+        ng-model='$ctrl.value'
+        ng-model-options='{allowInvalid: true}'
+        ng-change='$ctrl.onValueChange()'
+        name='numberInput'
+        placeholder='{{$ctrl.placeholder}}'
+        min='{{ $ctrl.min ? $ctrl.min / $ctrl.sizeScale.value : "" }}'
+        max='{{ $ctrl.max ? $ctrl.max / $ctrl.sizeScale.value : "" }}'
+        ng-required='$ctrl.required'
+        ng-disabled='$ctrl.ngDisabled'
+        ignite-form-field-input-autofocus='{{$ctrl.autofocus}}'
+        ng-on-blur='$ctrl.triggerBlur()'
+    )
+    button.select-toggle(
+        bs-select
+        bs-options='size as size.label for size in $ctrl.sizesMenu'
+        ng-model='$ctrl.sizeScale'
+        protect-from-bs-select-render
+        ng-disabled='$ctrl.ngDisabled'
+        type='button'
+    )
+        | {{ $ctrl.sizeScale.label }}
+
+.form-field__errors(
+    ng-messages='$ctrl.ngModel.$error'
+    ng-show=`($ctrl.ngModel.$dirty || $ctrl.ngModel.$touched || $ctrl.ngModel.$submitted) && $ctrl.ngModel.$invalid`
+)
+    div(ng-transclude)
+    +form-field__error({
+        error: 'required',
+        message: 'This field could not be empty'
+    })
+    +form-field__error({
+        error: 'min',
+        message: 'Value is less than allowable minimum: {{ $ctrl.min/$ctrl.sizeScale.value }} {{$ctrl.sizeScale.label}}'
+    })
+    +form-field__error({
+        error: 'max',
+        message: 'Value is more than allowable maximum: {{ $ctrl.max/$ctrl.sizeScale.value }} {{$ctrl.sizeScale.label}}'
+    })
+    +form-field__error({
+        error: 'number',
+        message: 'Only numbers allowed'
+    })
+    +form-field__error({
+        error: 'step',
+        message: 'Invalid step'
+    })
diff --git a/modules/frontend/app/components/form-field/copyInputValueButton.directive.js b/modules/frontend/app/components/form-field/copyInputValueButton.directive.js
new file mode 100644
index 0000000..06500e4
--- /dev/null
+++ b/modules/frontend/app/components/form-field/copyInputValueButton.directive.js
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const template = `
+    <svg
+        class='copy-input-value-button'
+        ignite-icon='copy'
+        ignite-copy-to-clipboard='{{ $ctrl.value }}'
+        bs-tooltip=''
+        data-title='{{::$ctrl.title}}'
+        ng-show='$ctrl.value'
+    ></svg>
+`;
+
+class CopyInputValueButtonController {
+    /** @type {ng.INgModelController} */
+    ngModel;
+
+    /**
+     * Tooltip title
+     * @type {string}
+     */
+    title;
+
+    static $inject = ['$element', '$compile', '$scope'];
+
+    /**
+     * @param {JQLite} $element
+     * @param {ng.ICompileService} $compile
+     * @param {ng.IScope} $scope
+     */
+    constructor($element, $compile, $scope) {
+        this.$element = $element;
+        this.$compile = $compile;
+        this.$scope = $scope;
+    }
+
+    $postLink() {
+        this.buttonScope = this.$scope.$new(true);
+        this.buttonScope.$ctrl = this;
+        this.$compile(template)(this.buttonScope, (clone) => {
+            this.$element[0].parentElement.appendChild(clone[0]);
+            this.buttonElement = clone;
+        });
+    }
+
+    $onDestroy() {
+        this.buttonScope.$ctrl = null;
+        this.buttonScope.$destroy();
+        this.buttonElement.remove();
+        this.buttonElement = this.$element = this.ngModel = null;
+    }
+
+    get value() {
+        return this.ngModel
+            ? this.ngModel.$modelValue
+            : void 0;
+    }
+}
+
+export function directive() {
+    return {
+        scope: false,
+        bindToController: {
+            title: '@copyInputValueButton'
+        },
+        controller: CopyInputValueButtonController,
+        require: {
+            ngModel: 'ngModel'
+        }
+    };
+}
diff --git a/modules/frontend/app/components/form-field/igniteFormField.directive.ts b/modules/frontend/app/components/form-field/igniteFormField.directive.ts
new file mode 100644
index 0000000..e85ed1a
--- /dev/null
+++ b/modules/frontend/app/components/form-field/igniteFormField.directive.ts
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {IInputErrorNotifier} from 'app/types';
+
+type IgniteFormFieldScope < T > = ng.IScope & ({$input: T} | {[name: string]: T});
+
+export class IgniteFormField<T> implements IInputErrorNotifier {
+    static animName = 'ignite-form-field__error-blink';
+    static eventName = 'webkitAnimationEnd oAnimationEnd msAnimationEnd animationend';
+    static $inject = ['$element', '$scope'];
+    onAnimEnd: () => any | null;
+
+    constructor(private $element: JQLite, private $scope: IgniteFormFieldScope<T>) {}
+
+    $postLink() {
+        this.onAnimEnd = () => this.$element.removeClass(IgniteFormField.animName);
+        this.$element.on(IgniteFormField.eventName, this.onAnimEnd);
+    }
+
+    $onDestroy() {
+        this.$element.off(IgniteFormField.eventName, this.onAnimEnd);
+        this.$element = this.onAnimEnd = null;
+    }
+
+    notifyAboutError() {
+        if (!this.$element)
+            return;
+
+        if (this.isTooltipValidation())
+            this.$element.find('.form-field__error [bs-tooltip]').trigger('mouseenter');
+        else
+            this.$element.addClass(IgniteFormField.animName);
+    }
+
+    hideError() {
+        if (!this.$element)
+            return;
+
+        if (this.isTooltipValidation())
+            this.$element.find('.form-field__error [bs-tooltip]').trigger('mouseleave');
+    }
+
+    isTooltipValidation(): boolean {
+        return !this.$element.parents('.theme--ignite-errors-horizontal').length;
+    }
+
+    /**
+     * Exposes control in $scope
+     */
+    exposeControl(control: ng.INgModelController, name = '$input') {
+        this.$scope[name] = control;
+        this.$scope.$on('$destroy', () => this.$scope[name] = null);
+    }
+}
+
+export function directive<T>(): ng.IDirective<IgniteFormFieldScope<T>> {
+    return {
+        restrict: 'C',
+        controller: IgniteFormField,
+        scope: true
+    };
+}
diff --git a/modules/frontend/app/components/form-field/index.js b/modules/frontend/app/components/form-field/index.js
new file mode 100644
index 0000000..3f9a620
--- /dev/null
+++ b/modules/frontend/app/components/form-field/index.js
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import './style.scss';
+import {directive as igniteFormField} from './igniteFormField.directive';
+import {directive as showValidationError} from './showValidationError.directive';
+import {directive as copyInputValue} from './copyInputValueButton.directive';
+
+import {default as formFieldSize} from './components/form-field-size';
+
+export default angular
+    .module('ignite-console.form-field', [])
+    .component('formFieldSize', formFieldSize)
+    .directive('igniteFormField', igniteFormField)
+    .directive('ngModel', showValidationError)
+    .directive('copyInputValueButton', copyInputValue);
diff --git a/modules/frontend/app/components/form-field/showValidationError.directive.ts b/modules/frontend/app/components/form-field/showValidationError.directive.ts
new file mode 100644
index 0000000..7743f7a
--- /dev/null
+++ b/modules/frontend/app/components/form-field/showValidationError.directive.ts
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {IInputErrorNotifier} from '../../types';
+
+const scrollIntoView = (() => {
+    if (HTMLElement.prototype.scrollIntoViewIfNeeded)
+        return (el: HTMLElement) => {el.scrollIntoViewIfNeeded();};
+    return (el: HTMLElement) => {
+        try {
+            el.scrollIntoView({block: 'center'});
+        } catch (e) {
+            el.scrollIntoView();
+        }
+    };
+})();
+
+/**
+ * Brings user attention to invalid form fields.
+ * Use IgniteFormUtils.triggerValidation to trigger the event.
+ */
+export function directive($timeout) {
+    return {
+        require: ['ngModel', '?^^bsCollapseTarget', '?^^igniteFormField', '?formFieldSize', '?^^panelCollapsible'],
+        link(scope, el, attr, [ngModel, bsCollapseTarget, igniteFormField, formFieldSize, panelCollapsible]) {
+            const formFieldController: IInputErrorNotifier = igniteFormField || formFieldSize;
+
+            let onBlur;
+
+            scope.$on('$destroy', () => {
+                el[0].removeEventListener('blur', onBlur);
+                onBlur = null;
+            });
+
+            const off = scope.$on('$showValidationError', (e, target) => {
+                if (target !== ngModel)
+                    return;
+
+                ngModel.$setTouched();
+
+                bsCollapseTarget && bsCollapseTarget.open();
+                panelCollapsible && panelCollapsible.open();
+
+                if (!onBlur && formFieldController) {
+                    onBlur = () => formFieldController.hideError();
+
+                    el[0].addEventListener('blur', onBlur, {passive: true});
+                }
+
+                $timeout(() => {
+                    scrollIntoView(el[0]);
+
+                    if (!attr.bsSelect)
+                        $timeout(() => el[0].focus(), 100);
+
+                    formFieldController && formFieldController.notifyAboutError();
+                });
+            });
+        }
+    };
+}
+
+directive.$inject = ['$timeout'];
diff --git a/modules/frontend/app/components/form-field/style.scss b/modules/frontend/app/components/form-field/style.scss
new file mode 100644
index 0000000..2cf767f
--- /dev/null
+++ b/modules/frontend/app/components/form-field/style.scss
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.copy-input-value-button {
+    position: absolute;
+    top: 31px;
+    right: 10px;
+
+    &:hover {
+        @import 'public/stylesheets/variables';
+
+        color: $ignite-brand-success;
+        cursor: pointer;
+    }
+}
diff --git a/modules/frontend/app/components/form-signup/component.ts b/modules/frontend/app/components/form-signup/component.ts
new file mode 100644
index 0000000..79863db
--- /dev/null
+++ b/modules/frontend/app/components/form-signup/component.ts
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+import {FormSignup} from './controller';
+
+export const component: ng.IComponentOptions = {
+    template,
+    controller: FormSignup,
+    bindings: {
+        outerForm: '<',
+        serverError: '<'
+    },
+    require: {
+        ngModel: 'ngModel'
+    }
+};
diff --git a/modules/frontend/app/components/form-signup/controller.ts b/modules/frontend/app/components/form-signup/controller.ts
new file mode 100644
index 0000000..eedf6d0
--- /dev/null
+++ b/modules/frontend/app/components/form-signup/controller.ts
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import CountriesService from '../../services/Countries.service';
+import {ISignupFormController} from '.';
+
+export class FormSignup implements ng.IPostLink, ng.IOnDestroy, ng.IOnChanges {
+    static $inject = ['IgniteCountries'];
+
+    constructor(private Countries: ReturnType<typeof CountriesService>) {}
+
+    countries = this.Countries.getAll();
+
+    innerForm: ISignupFormController;
+    outerForm: ng.IFormController;
+    ngModel: ng.INgModelController;
+    serverError: string | null = null;
+
+    $postLink() {
+        this.outerForm.$addControl(this.innerForm);
+        this.innerForm.email.$validators.server = () => !this.serverError;
+    }
+
+    $onDestroy() {
+        this.outerForm.$removeControl(this.innerForm);
+    }
+
+    $onChanges(changes: {serverError: ng.IChangesObject<FormSignup['serverError']>}) {
+        if (changes.serverError && this.innerForm)
+            this.innerForm.email.$validate();
+    }
+}
diff --git a/modules/frontend/app/components/form-signup/index.ts b/modules/frontend/app/components/form-signup/index.ts
new file mode 100644
index 0000000..1798aa0
--- /dev/null
+++ b/modules/frontend/app/components/form-signup/index.ts
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {component} from './component';
+
+export default angular.module('ignite-console.form-signup', [])
+    .component('formSignup', component);
+
+export interface ISignupData {
+    email: string,
+    password: string,
+    firstName: string,
+    lastName: string,
+    phone?: string,
+    company: string,
+    country: string
+}
+
+export interface ISignupFormController extends ng.IFormController {
+    email: ng.INgModelController,
+    password: ng.INgModelController,
+    firstName: ng.INgModelController,
+    lastName: ng.INgModelController,
+    phone: ng.INgModelController,
+    company: ng.INgModelController,
+    country: ng.INgModelController
+}
diff --git a/modules/frontend/app/components/form-signup/style.scss b/modules/frontend/app/components/form-signup/style.scss
new file mode 100644
index 0000000..e80b510
--- /dev/null
+++ b/modules/frontend/app/components/form-signup/style.scss
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+form-signup {
+    .form-signup__grid {
+        display: grid;
+        grid-gap: 10px;
+        grid-template-columns: 1fr 1fr;
+
+        .span-1 {
+            grid-column: span 1;
+        }
+        .span-2 {
+            grid-column: span 2;
+        }
+    }
+}
diff --git a/modules/frontend/app/components/form-signup/template.pug b/modules/frontend/app/components/form-signup/template.pug
new file mode 100644
index 0000000..b9ed948
--- /dev/null
+++ b/modules/frontend/app/components/form-signup/template.pug
@@ -0,0 +1,105 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+.form-signup__grid(ng-form='signup' ng-ref='$ctrl.innerForm' ng-ref-read='form')
+    .span-2
+        +form-field__email({
+            label: 'Email:',
+            model: '$ctrl.ngModel.$viewValue.email',
+            name: '"email"',
+            placeholder: 'Input email',
+            required: true
+        })(
+            ng-model-options='{allowInvalid: true}'
+            autocomplete='email'
+            ignite-auto-focus
+        )
+            +form-field__error({error: 'server', message: `{{$ctrl.serverError}}`})
+    .span-1
+        +form-field__password({
+            label: 'Password:',
+            model: '$ctrl.ngModel.$viewValue.password',
+            name: '"password"',
+            placeholder: 'Input password',
+            required: true
+        })(
+            autocomplete='new-password'
+        )
+    .span-1
+        +form-field__password({
+            label: 'Confirm:',
+            model: 'confirm',
+            name: '"confirm"',
+            placeholder: 'Confirm password',
+            required: true
+        })(
+            ignite-match='$ctrl.ngModel.$viewValue.password'
+            autocomplete='off'
+        )
+    .span-1
+        +form-field__text({
+            label: 'First name:',
+            model: '$ctrl.ngModel.$viewValue.firstName',
+            name: '"firstName"',
+            placeholder: 'Input first name',
+            required: true
+        })(
+            autocomplete='given-name'
+        )
+    .span-1
+        +form-field__text({
+            label: 'Last name:',
+            model: '$ctrl.ngModel.$viewValue.lastName',
+            name: '"lastName"',
+            placeholder: 'Input last name',
+            required: true
+        })(
+            autocomplete='family-name'
+        )
+    .span-1
+        +form-field__phone({
+            label: 'Phone:',
+            model: '$ctrl.ngModel.$viewValue.phone',
+            name: '"phone"',
+            placeholder: 'Input phone (ex.: +15417543010)',
+            optional: true
+        })(
+            autocomplete='tel'
+        )
+    .span-1
+        +form-field__dropdown({
+            label: 'Country/Region:',
+            model: '$ctrl.ngModel.$viewValue.country',
+            name: '"country"',
+            required: true,
+            placeholder: 'Choose your country',
+            options: '$ctrl.countries'
+        })(
+            autocomplete='country'
+        )
+    .span-2
+        +form-field__text({
+            label: 'Company:',
+            model: '$ctrl.ngModel.$viewValue.company',
+            name: '"company"',
+            placeholder: 'Input company name',
+            required: true
+        })(
+            ignite-on-enter-focus-move='countryInput'
+            autocomplete='organization'
+        )
diff --git a/modules/frontend/app/components/global-progress-line/component.ts b/modules/frontend/app/components/global-progress-line/component.ts
new file mode 100644
index 0000000..508bb93
--- /dev/null
+++ b/modules/frontend/app/components/global-progress-line/component.ts
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import controller from './controller';
+import template from './template.pug';
+import './style.scss';
+
+export default {
+    template,
+    controller,
+    bindings: {
+        isLoading: '<'
+    }
+} as ng.IComponentOptions;
diff --git a/modules/frontend/app/components/global-progress-line/controller.ts b/modules/frontend/app/components/global-progress-line/controller.ts
new file mode 100644
index 0000000..fd8a4bb
--- /dev/null
+++ b/modules/frontend/app/components/global-progress-line/controller.ts
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class GlobalProgressLine {
+    /** @type {boolean} */
+    isLoading;
+
+    static $inject = ['$element', '$document', '$scope'];
+
+    _child: Element;
+
+    constructor(private $element: JQLite, private $document: ng.IDocumentService, private $scope: ng.IScope) {}
+
+    $onChanges() {
+        this.$scope.$evalAsync(() => {
+            if (this.isLoading) {
+                this._child = this.$element[0].querySelector('.global-progress-line__progress-line');
+
+                if (this._child)
+                    this.$document[0].querySelector('web-console-header').appendChild(this._child);
+            }
+            else
+                this.$element.hide();
+        });
+    }
+
+    $onDestroy() {
+        if (this._child) {
+            this._child.parentElement.removeChild(this._child);
+            this._child = null;
+        }
+    }
+}
diff --git a/modules/frontend/app/components/global-progress-line/index.ts b/modules/frontend/app/components/global-progress-line/index.ts
new file mode 100644
index 0000000..8ec4982
--- /dev/null
+++ b/modules/frontend/app/components/global-progress-line/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import component from './component';
+
+export default angular
+    .module('ignite-console.global-progress-line', [])
+    .component('globalProgressLine', component);
diff --git a/modules/frontend/app/components/global-progress-line/style.scss b/modules/frontend/app/components/global-progress-line/style.scss
new file mode 100644
index 0000000..767d606
--- /dev/null
+++ b/modules/frontend/app/components/global-progress-line/style.scss
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+web-console-header .global-progress-line__progress-line {
+    height: 4px;
+    position: absolute;
+    bottom: -4px;
+    left: 0;
+    right: 0;
+    --background-color: white;
+}
diff --git a/modules/frontend/app/components/global-progress-line/template.pug b/modules/frontend/app/components/global-progress-line/template.pug
new file mode 100644
index 0000000..701e794
--- /dev/null
+++ b/modules/frontend/app/components/global-progress-line/template.pug
@@ -0,0 +1,17 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+progress-line.global-progress-line__progress-line(value='$ctrl.isLoading ? -1 : 1' )
diff --git a/modules/frontend/app/components/grid-column-selector/component.js b/modules/frontend/app/components/grid-column-selector/component.js
new file mode 100644
index 0000000..0ad3711
--- /dev/null
+++ b/modules/frontend/app/components/grid-column-selector/component.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+import './style.scss';
+
+export default {
+    template,
+    controller,
+    transclude: true,
+    bindings: {
+        gridApi: '<'
+    }
+};
diff --git a/modules/frontend/app/components/grid-column-selector/controller.js b/modules/frontend/app/components/grid-column-selector/controller.js
new file mode 100644
index 0000000..2f5554e
--- /dev/null
+++ b/modules/frontend/app/components/grid-column-selector/controller.js
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import uniq from 'lodash/fp/uniq';
+import difference from 'lodash/difference';
+import findLast from 'lodash/findLast';
+
+const hasGrouping = (changes) => {
+    return changes.gridApi.currentValue !== changes.gridApi.previousValue && changes.gridApi.currentValue.grouping;
+};
+const id = (column) => column.categoryDisplayName || column.displayName || column.name;
+const picksrc = (state) => state.categories.length ? state.categories : state.columns;
+
+export default class GridColumnSelectorController {
+    static $inject = ['$scope', 'uiGridConstants'];
+
+    constructor($scope, uiGridConstants) {
+        Object.assign(this, {$scope, uiGridConstants});
+    }
+
+    $onChanges(changes) {
+        if (changes && 'gridApi' in changes && changes.gridApi.currentValue) {
+            this.applyValues();
+            this.gridApi.grid.registerDataChangeCallback(() => this.applyValues(), [this.uiGridConstants.dataChange.COLUMN]);
+            if (hasGrouping(changes)) this.gridApi.grouping.on.groupingChanged(this.$scope, () => this.applyValues());
+        }
+    }
+
+    applyValues() {
+        this.state = this.getState();
+        this.columnsMenu = this.makeMenu();
+        this.selectedColumns = this.getSelectedColumns();
+        this.setSelectedColumns();
+    }
+
+    getSelectedColumns() {
+        return picksrc(this.state).filter((i) => i.isVisible && i.isInMenu).map((i) => id(i.item));
+    }
+
+    makeMenu() {
+        return picksrc(this.state).filter((i) => i.isInMenu).map((i) => ({
+            name: id(i.item),
+            item: i.item
+        }));
+    }
+
+    getState() {
+        const api = this.gridApi;
+        const columns = api.grid.options.columnDefs;
+        const categories = api.grid.options.categories || [];
+        const grouping = api.grouping
+            ? api.grouping.getGrouping().grouping.map((g) => columns.find((c) => c.name === g.colName).categoryDisplayName)
+            : [];
+        const mapfn = (item) => ({
+            item,
+            isInMenu: item.enableHiding !== false && !grouping.includes(item.name),
+            isVisible: grouping.includes(item.name) || item.visible !== false
+        });
+        return ({
+            categories: categories.map(mapfn),
+            columns: columns.map(mapfn)
+        });
+    }
+
+    findScrollToNext(columns, prevColumns) {
+        if (!prevColumns)
+            return;
+
+        const diff = difference(columns, prevColumns);
+
+        if (diff.length === 1 && columns.includes(diff[0]))
+            return diff[0];
+    }
+
+    setSelectedColumns() {
+        const {selectedColumns} = this;
+        const scrollToNext = this.findScrollToNext(selectedColumns, this.prevSelectedColumns);
+        this.prevSelectedColumns = selectedColumns;
+        const all = this.state.categories.concat(this.state.columns);
+        const itemsToShow = uniq(all.filter((i) => !i.isInMenu && i.isVisible).map((i) => id(i.item)).concat(selectedColumns));
+        (this.gridApi.grid.options.categories || []).concat(this.gridApi.grid.options.columnDefs).forEach((item) => {
+            item.visible = itemsToShow.includes(id(item));
+        });
+        // Scrolls to the last matching columnDef, useful if it was out of view after being enabled.
+        this.refreshColumns().then(() => {
+            if (scrollToNext) {
+                const column = findLast(this.gridApi.grid.options.columnDefs, (c) => id(c) === scrollToNext);
+                this.gridApi.grid.scrollTo(null, column);
+            }
+        });
+    }
+
+    // gridApi.grid.refreshColumns method does not allow to wait until async operation completion.
+    // This method does roughly the same, but returns a Promise.
+    refreshColumns() {
+        return this.gridApi.grid.processColumnsProcessors(this.gridApi.grid.columns)
+        .then((renderableColumns) => this.gridApi.grid.setVisibleColumns(renderableColumns))
+        .then(() => this.gridApi.grid.redrawInPlace())
+        .then(() => this.gridApi.grid.refreshCanvas(true));
+    }
+}
diff --git a/modules/frontend/app/components/grid-column-selector/controller.spec.js b/modules/frontend/app/components/grid-column-selector/controller.spec.js
new file mode 100644
index 0000000..d163a9c
--- /dev/null
+++ b/modules/frontend/app/components/grid-column-selector/controller.spec.js
@@ -0,0 +1,435 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the License); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an AS IS BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {suite, test} from 'mocha';
+import {assert} from 'chai';
+import {spy, stub} from 'sinon';
+
+import Controller from './controller';
+
+const mocks = () => new Map([
+    ['$scope', {}],
+    ['uiGridConstants', {
+        dataChange: {
+            COLUMN: 'COLUMN'
+        }
+    }]
+]);
+
+const apiMock = () => ({
+    grid: {
+        options: {
+            columnDefs: []
+        },
+        registerDataChangeCallback: spy(),
+        processColumnsProcessors: spy((v) => Promise.resolve(v)),
+        setVisibleColumns: spy((v) => Promise.resolve(v)),
+        redrawInPlace: spy((v) => Promise.resolve(v)),
+        refreshCanvas: spy((v) => Promise.resolve(v)),
+        scrollTo: spy()
+    },
+    grouping: {
+        on: {
+            groupingChanged: spy()
+        },
+        getGrouping: stub().returns({grouping: []})
+    }
+});
+
+suite('grid-column-selector component controller', () => {
+    test('$onChanges', () => {
+        const c = new Controller(...mocks().values());
+        c.applyValues = spy(c.applyValues.bind(c));
+        const api = apiMock();
+        c.gridApi = api;
+        c.$onChanges({gridApi: {currentValue: api}});
+
+        assert.equal(c.applyValues.callCount, 1, 'calls applyValues');
+        assert.isFunction(
+            c.gridApi.grid.registerDataChangeCallback.lastCall.args[0]
+        );
+
+        c.gridApi.grid.registerDataChangeCallback.lastCall.args[0]();
+
+        assert.equal(
+            c.applyValues.callCount,
+            2,
+            'registers applyValues as data change callback'
+        );
+        assert.deepEqual(
+            c.gridApi.grid.registerDataChangeCallback.lastCall.args[1],
+            [c.uiGridConstants.dataChange.COLUMN],
+            'registers data change callback for COLUMN'
+        );
+        assert.equal(
+            c.gridApi.grouping.on.groupingChanged.lastCall.args[0],
+            c.$scope,
+            'registers grouping change callback with correct $scope'
+        );
+
+        c.gridApi.grouping.on.groupingChanged.lastCall.args[1]();
+
+        assert.equal(
+            c.applyValues.callCount,
+            3,
+            'registers applyValues as grouping change callback'
+        );
+    });
+    test('applyValues', () => {
+        const c = new Controller(...mocks().values());
+        const mock = {
+            getState: stub().returns({}),
+            makeMenu: stub().returns({}),
+            getSelectedColumns: stub().returns({}),
+            setSelectedColumns: spy()
+        };
+        c.applyValues.call(mock);
+
+        assert.equal(
+            mock.state,
+            mock.getState.lastCall.returnValue,
+            'assigns getState return value as this.state'
+        );
+        assert.equal(
+            mock.columnsMenu,
+            mock.makeMenu.lastCall.returnValue,
+            'assigns makeMenu return value as this.columnsMenu'
+        );
+        assert.equal(
+            mock.selectedColumns,
+            mock.getSelectedColumns.lastCall.returnValue,
+            'assigns getSelectedColumns return value as this.selectedColumns'
+        );
+        assert.equal(
+            mock.setSelectedColumns.callCount,
+            1,
+            'calls setSelectedColumns once'
+        );
+    });
+    test('getSelectedColumns, using categories', () => {
+        const c = new Controller(...mocks().values());
+        c.state = {
+            categories: [
+                {isVisible: false},
+                {isVisible: true, isInMenu: false},
+                {isVisible: true, isInMenu: true, item: {categoryDisplayName: '1'}},
+                {isVisible: true, isInMenu: true, item: {displayName: '2'}},
+                {isVisible: true, isInMenu: true, item: {name: '3'}}
+            ],
+            columns: []
+        };
+
+        assert.deepEqual(
+            c.getSelectedColumns(),
+            ['1', '2', '3'],
+            'returns correct value, prefers categories over columns'
+        );
+    });
+    test('getSelectedColumns, using columnDefs', () => {
+        const c = new Controller(...mocks().values());
+        c.state = {
+            categories: [
+            ],
+            columns: [
+                {isVisible: false},
+                {isVisible: true, isInMenu: false},
+                {isVisible: true, isInMenu: true, item: {categoryDisplayName: '1'}},
+                {isVisible: true, isInMenu: true, item: {displayName: '2'}},
+                {isVisible: true, isInMenu: true, item: {name: '3'}}
+            ]
+        };
+
+        assert.deepEqual(
+            c.getSelectedColumns(),
+            ['1', '2', '3'],
+            'returns correct value, uses columns if there are no categories'
+        );
+    });
+    test('makeMenu, using categories', () => {
+        const c = new Controller(...mocks().values());
+        c.state = {
+            categories: [
+                {isVisible: false},
+                {isVisible: true, isInMenu: false},
+                {isVisible: true, isInMenu: true, item: {categoryDisplayName: '1'}},
+                {isVisible: true, isInMenu: true, item: {displayName: '2'}},
+                {isVisible: true, isInMenu: true, item: {name: '3'}}
+            ],
+            columns: []
+        };
+
+        assert.deepEqual(
+            c.makeMenu(),
+            [
+                {item: {categoryDisplayName: '1'}, name: '1'},
+                {item: {displayName: '2'}, name: '2'},
+                {item: {name: '3'}, name: '3'}
+            ],
+            'returns correct value, prefers categories over columns'
+        );
+    });
+    test('makeMenu, using columns', () => {
+        const c = new Controller(...mocks().values());
+        c.state = {
+            categories: [],
+            columns: [
+                {isVisible: false},
+                {isVisible: true, isInMenu: false},
+                {isVisible: true, isInMenu: true, item: {categoryDisplayName: '1'}},
+                {isVisible: true, isInMenu: true, item: {displayName: '2'}},
+                {isVisible: true, isInMenu: true, item: {name: '3'}}
+            ]
+        };
+
+        assert.deepEqual(
+            c.makeMenu(),
+            [
+                {item: {categoryDisplayName: '1'}, name: '1'},
+                {item: {displayName: '2'}, name: '2'},
+                {item: {name: '3'}, name: '3'}
+            ],
+            'returns correct value, uses columns if there are no categories'
+        );
+    });
+    test('getState', () => {
+        const c = new Controller(...mocks().values());
+        c.gridApi = apiMock();
+        c.gridApi.grouping.getGrouping = () => ({grouping: [{colName: 'a', categoryDisplayName: 'A'}]});
+        c.gridApi.grid.options.columnDefs = [
+            {visible: false, name: 'a', categoryDisplayName: 'A'},
+            {visible: true, name: 'a1', categoryDisplayName: 'A'},
+            {visible: true, name: 'a2', categoryDisplayName: 'A'},
+            {visible: true, name: 'b1', categoryDisplayName: 'B'},
+            {visible: true, name: 'b2', categoryDisplayName: 'B'},
+            {visible: true, name: 'b3', categoryDisplayName: 'B'},
+            {visible: true, name: 'c1', categoryDisplayName: 'C'},
+            {visible: true, name: 'c2', categoryDisplayName: 'C', enableHiding: false},
+            {visible: false, name: 'c3', categoryDisplayName: 'C'}
+        ];
+        c.gridApi.grid.options.categories = [
+            {categoryDisplayName: 'A', enableHiding: false, visible: true},
+            {categoryDisplayName: 'B', enableHiding: true, visible: false},
+            {categoryDisplayName: 'C', enableHiding: true, visible: true}
+        ];
+
+        assert.deepEqual(
+            c.getState(),
+            {
+                categories: [{
+                    isInMenu: false,
+                    isVisible: true,
+                    item: {
+                        categoryDisplayName: 'A',
+                        enableHiding: false,
+                        visible: true
+                    }
+                }, {
+                    isInMenu: true,
+                    isVisible: false,
+                    item: {
+                        categoryDisplayName: 'B',
+                        enableHiding: true,
+                        visible: false
+                    }
+                }, {
+                    isInMenu: true,
+                    isVisible: true,
+                    item: {
+                        categoryDisplayName: 'C',
+                        enableHiding: true,
+                        visible: true
+                    }
+                }],
+                columns: [{
+                    isInMenu: true,
+                    isVisible: false,
+                    item: {
+                        categoryDisplayName: 'A',
+                        name: 'a',
+                        visible: false
+                    }
+                }, {
+                    isInMenu: true,
+                    isVisible: true,
+                    item: {
+                        categoryDisplayName: 'A',
+                        name: 'a1',
+                        visible: true
+                    }
+                }, {
+                    isInMenu: true,
+                    isVisible: true,
+                    item: {
+                        categoryDisplayName: 'A',
+                        name: 'a2',
+                        visible: true
+                    }
+                }, {
+                    isInMenu: true,
+                    isVisible: true,
+                    item: {
+                        categoryDisplayName: 'B',
+                        name: 'b1',
+                        visible: true
+                    }
+                }, {
+                    isInMenu: true,
+                    isVisible: true,
+                    item: {
+                        categoryDisplayName: 'B',
+                        name: 'b2',
+                        visible: true
+                    }
+                }, {
+                    isInMenu: true,
+                    isVisible: true,
+                    item: {
+                        categoryDisplayName: 'B',
+                        name: 'b3',
+                        visible: true
+                    }
+                }, {
+                    isInMenu: true,
+                    isVisible: true,
+                    item: {
+                        categoryDisplayName: 'C',
+                        name: 'c1',
+                        visible: true
+                    }
+                }, {
+                    isInMenu: false,
+                    isVisible: true,
+                    item: {
+                        categoryDisplayName: 'C',
+                        name: 'c2',
+                        visible: true,
+                        enableHiding: false
+                    }
+                }, {
+                    isInMenu: true,
+                    isVisible: false,
+                    item: {
+                        categoryDisplayName: 'C',
+                        name: 'c3',
+                        visible: false
+                    }
+                }]
+            },
+            'returns correct value'
+        );
+    });
+    test('findScrollToNext', () => {
+        assert.deepEqual(
+            Controller.prototype.findScrollToNext([1, 2, 3], [1, 2]),
+            3,
+            `returns new item if it's in selected collection end`
+        );
+        assert.deepEqual(
+            Controller.prototype.findScrollToNext([1, 2], [1, 3]),
+            2,
+            `returns new item if it's in selected collection middle`
+        );
+        assert.deepEqual(
+            Controller.prototype.findScrollToNext([1, 2, 3], [2, 3]),
+            1,
+            `returns new item if it's in selected collection start`
+        );
+        assert.equal(
+            Controller.prototype.findScrollToNext([1, 2, 3, 4, 5], [1]),
+            void 0,
+            `returns nothing if there's more than one new item`
+        );
+        assert.equal(
+            Controller.prototype.findScrollToNext([1, 2], [1, 2, 3]),
+            void 0,
+            `returns nothing if items were removed`
+        );
+    });
+    test('setSelectedColumns', () => {
+        const c = new Controller(...mocks().values());
+        const api = c.gridApi = apiMock();
+        c.refreshColumns = stub().returns(({then: (f) => f()}));
+        c.gridApi.grid.options.columnDefs = [
+            {name: 'a', visible: false, categoryDisplayName: 'A'},
+            {name: 'a1', visible: false, categoryDisplayName: 'A'},
+            {name: 'b', visible: true, categoryDisplayName: 'B'}
+        ];
+        c.gridApi.grid.options.categories = [
+            {categoryDisplayName: 'A', visible: false},
+            {categoryDisplayName: 'B'}
+        ];
+        c.$onChanges({gridApi: {currentValue: api}});
+        c.selectedColumns = ['A', 'B'];
+        c.setSelectedColumns();
+
+        assert.equal(
+            c.refreshColumns.callCount,
+            2,
+            'calls refreshColumns'
+        );
+        assert.deepEqual(
+            c.gridApi.grid.options.categories,
+            [
+                {categoryDisplayName: 'A', visible: true},
+                {categoryDisplayName: 'B', visible: true}
+            ],
+            'changes category visibility'
+        );
+        assert.deepEqual(
+            c.gridApi.grid.options.columnDefs,
+            [
+                {name: 'a', visible: true, categoryDisplayName: 'A'},
+                {name: 'a1', visible: true, categoryDisplayName: 'A'},
+                {name: 'b', visible: true, categoryDisplayName: 'B'}
+            ],
+            'changes column visibility'
+        );
+        assert.deepEqual(
+            c.gridApi.grid.scrollTo.lastCall.args,
+            [null, {name: 'a1', visible: true, categoryDisplayName: 'A'}],
+            'scrolls to last added column'
+        );
+    });
+    test('refreshColumns', () => {
+        const c = new Controller(...mocks().values());
+        c.gridApi = apiMock();
+        c.gridApi.grid.columns = [1, 2, 3];
+
+        return c.refreshColumns().then(() => {
+            assert.equal(
+                c.gridApi.grid.processColumnsProcessors.lastCall.args[0],
+                c.gridApi.grid.columns,
+                'calls processColumnsProcessors with correct args'
+            );
+            assert.equal(
+                c.gridApi.grid.setVisibleColumns.lastCall.args[0],
+                c.gridApi.grid.columns,
+                'calls setVisibleColumns with correct args'
+            );
+            assert.equal(
+                c.gridApi.grid.redrawInPlace.callCount,
+                1,
+                'calls redrawInPlace'
+            );
+            assert.equal(
+                c.gridApi.grid.refreshCanvas.lastCall.args[0],
+                true,
+                'calls refreshCanvas with correct args'
+            );
+        });
+    });
+});
diff --git a/modules/frontend/app/components/grid-column-selector/index.js b/modules/frontend/app/components/grid-column-selector/index.js
new file mode 100644
index 0000000..16ef655
--- /dev/null
+++ b/modules/frontend/app/components/grid-column-selector/index.js
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import component from './component';
+
+export default angular
+    .module('ignite-console.grid-column-selector', [])
+    .component('gridColumnSelector', component);
diff --git a/modules/frontend/app/components/grid-column-selector/style.scss b/modules/frontend/app/components/grid-column-selector/style.scss
new file mode 100644
index 0000000..872d727
--- /dev/null
+++ b/modules/frontend/app/components/grid-column-selector/style.scss
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+grid-column-selector {
+    display: inline-block;
+    line-height: initial;
+
+    .btn-ignite, .icon {
+        margin: 0 !important;
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/components/grid-column-selector/template.pug b/modules/frontend/app/components/grid-column-selector/template.pug
new file mode 100644
index 0000000..afb246e
--- /dev/null
+++ b/modules/frontend/app/components/grid-column-selector/template.pug
@@ -0,0 +1,29 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+button.btn-ignite.btn-ignite--link-dashed-secondary(
+    protect-from-bs-select-render
+    bs-select
+    ng-model='$ctrl.selectedColumns'
+    ng-change='$ctrl.setSelectedColumns()'
+    ng-model-options='{debounce: {default: 5}}',
+    bs-options='column.name as column.name for column in $ctrl.columnsMenu'
+    bs-on-before-show='$ctrl.onShow'
+    data-multiple='true'
+    ng-transclude
+    ng-show='$ctrl.columnsMenu.length'
+)
+    svg(ignite-icon='gear').icon
diff --git a/modules/frontend/app/components/grid-export/component.js b/modules/frontend/app/components/grid-export/component.js
new file mode 100644
index 0000000..d4cfb29
--- /dev/null
+++ b/modules/frontend/app/components/grid-export/component.js
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import {CSV} from 'app/services/CSV';
+
+export default {
+    template,
+    controller: class {
+        static $inject = ['$scope', 'uiGridGroupingConstants', 'uiGridExporterService', 'uiGridExporterConstants', 'CSV'];
+
+        /**
+         * @param {CSV} CSV
+         */
+        constructor($scope, uiGridGroupingConstants, uiGridExporterService, uiGridExporterConstants, CSV) {
+            this.CSV = CSV;
+            Object.assign(this, { uiGridGroupingConstants, uiGridExporterService, uiGridExporterConstants });
+        }
+
+        export() {
+            const data = [];
+            const grid = this.gridApi.grid;
+            const exportColumnHeaders = this.uiGridExporterService.getColumnHeaders(grid, this.uiGridExporterConstants.VISIBLE);
+
+            grid.rows.forEach((row) => {
+                if (!row.visible)
+                    return;
+
+                const values = [];
+
+                exportColumnHeaders.forEach((exportCol) => {
+                    const col = grid.columns.find(({ field }) => field === exportCol.name);
+
+                    if (!col || !col.visible || col.colDef.exporterSuppressExport === true)
+                        return;
+
+                    const value = grid.getCellValue(row, col);
+
+                    values.push({ value });
+                });
+
+                data.push(values);
+            });
+
+            const csvContent = this.uiGridExporterService.formatAsCsv(exportColumnHeaders, data, this.CSV.getSeparator());
+
+            const csvFileName = this.fileName || 'export.csv';
+
+            this.uiGridExporterService.downloadFile(csvFileName, csvContent, this.gridApi.grid.options.exporterOlderExcelCompatibility);
+        }
+    },
+    bindings: {
+        gridApi: '<',
+        fileName: '<'
+    }
+};
diff --git a/modules/frontend/app/components/grid-export/index.js b/modules/frontend/app/components/grid-export/index.js
new file mode 100644
index 0000000..00e2bce
--- /dev/null
+++ b/modules/frontend/app/components/grid-export/index.js
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import './style.scss';
+import component from './component';
+
+export default angular
+    .module('ignite-console.grid-export', [])
+    .component('gridExport', component);
diff --git a/modules/frontend/app/components/grid-export/style.scss b/modules/frontend/app/components/grid-export/style.scss
new file mode 100644
index 0000000..29e35fd
--- /dev/null
+++ b/modules/frontend/app/components/grid-export/style.scss
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+grid-export {
+	&:not(.link-success) {
+		button:nth-child(2) {
+			display: none;
+		}
+	}
+
+	&.link-success {
+		button:nth-child(1) {
+			display: none;
+		}
+	}
+
+	[ignite-icon='download'] {
+		margin-right: 5px;
+	}
+}
diff --git a/modules/frontend/app/components/grid-export/template.pug b/modules/frontend/app/components/grid-export/template.pug
new file mode 100644
index 0000000..a085db6
--- /dev/null
+++ b/modules/frontend/app/components/grid-export/template.pug
@@ -0,0 +1,22 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+button.btn-ignite.btn-ignite--primary-outline(ng-click='$ctrl.export()' bs-tooltip='' data-title='Export filtered rows to CSV' data-placement='top')
+    svg(ignite-icon='csv')
+
+button.btn-ignite.btn-ignite--link-success.link-success(ng-click='$ctrl.export()' bs-tooltip='' data-title='Export filtered rows to CSV' data-placement='top')
+    svg(ignite-icon='download')
+    i Export to .csv
\ No newline at end of file
diff --git a/modules/frontend/app/components/grid-item-selected/component.js b/modules/frontend/app/components/grid-item-selected/component.js
new file mode 100644
index 0000000..c5b10d3
--- /dev/null
+++ b/modules/frontend/app/components/grid-item-selected/component.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+
+export default {
+    template,
+    controller,
+    transclude: true,
+    bindings: {
+        gridApi: '<'
+    }
+};
diff --git a/modules/frontend/app/components/grid-item-selected/controller.js b/modules/frontend/app/components/grid-item-selected/controller.js
new file mode 100644
index 0000000..ab35849
--- /dev/null
+++ b/modules/frontend/app/components/grid-item-selected/controller.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class {
+    static $inject = ['$scope', 'uiGridConstants'];
+
+    constructor($scope, uiGridConstants) {
+        Object.assign(this, {$scope, uiGridConstants});
+    }
+
+    $onChanges(changes) {
+        if (changes && 'gridApi' in changes && changes.gridApi.currentValue && this.gridApi.selection) {
+            this.applyValues();
+
+            this.gridApi.grid.registerDataChangeCallback(() => this.applyValues(), [this.uiGridConstants.dataChange.ROW]);
+            // Used to toggle selected of one row.
+            this.gridApi.selection.on.rowSelectionChanged(this.$scope, () => this.applyValues());
+            // Used to toggle all selected of rows.
+            this.gridApi.selection.on.rowSelectionChangedBatch(this.$scope, () => this.applyValues());
+        }
+    }
+
+    applyValues() {
+        this.selected = this.gridApi.selection.legacyGetSelectedRows().length;
+        this.count = this.gridApi.grid.rows.length;
+    }
+}
diff --git a/modules/frontend/app/components/grid-item-selected/index.js b/modules/frontend/app/components/grid-item-selected/index.js
new file mode 100644
index 0000000..4d76e6e
--- /dev/null
+++ b/modules/frontend/app/components/grid-item-selected/index.js
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import './style.scss';
+import component from './component';
+
+export default angular
+    .module('ignite-console.grid-item-selected', [])
+    .component('gridItemSelected', component);
diff --git a/modules/frontend/app/components/grid-item-selected/style.scss b/modules/frontend/app/components/grid-item-selected/style.scss
new file mode 100644
index 0000000..b131d82
--- /dev/null
+++ b/modules/frontend/app/components/grid-item-selected/style.scss
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+grid-item-selected {
+    display: inline-flex;
+    align-items: center;
+
+    font-size: 14px;
+}
diff --git a/modules/frontend/app/components/grid-item-selected/template.pug b/modules/frontend/app/components/grid-item-selected/template.pug
new file mode 100644
index 0000000..cf0695b
--- /dev/null
+++ b/modules/frontend/app/components/grid-item-selected/template.pug
@@ -0,0 +1,20 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+span(ng-if='$ctrl.count')
+    i {{ $ctrl.selected }} of {{ $ctrl.count }} selected
+span(ng-if='!$ctrl.count')
+    i Showing: 0 rows
\ No newline at end of file
diff --git a/modules/frontend/app/components/grid-no-data/component.js b/modules/frontend/app/components/grid-no-data/component.js
new file mode 100644
index 0000000..e7e26b8
--- /dev/null
+++ b/modules/frontend/app/components/grid-no-data/component.js
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './style.scss';
+import controller from './controller';
+
+export default {
+    template: `
+        <div ng-if='$ctrl.noData' ng-transclude></div>
+        <div ng-if='$ctrl.noDataFiltered' ng-transclude='noDataFiltered'></div>
+    `,
+    controller,
+    bindings: {
+        gridApi: '<'
+    },
+    transclude: {
+        noDataFiltered: '?gridNoDataFiltered'
+    }
+};
diff --git a/modules/frontend/app/components/grid-no-data/controller.js b/modules/frontend/app/components/grid-no-data/controller.js
new file mode 100644
index 0000000..d252c58
--- /dev/null
+++ b/modules/frontend/app/components/grid-no-data/controller.js
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+export default class {
+    static $inject = ['$scope', 'uiGridConstants'];
+
+    constructor($scope, uiGridConstants) {
+        Object.assign(this, {$scope, uiGridConstants});
+
+        this.noData = true;
+    }
+
+    $onChanges(changes) {
+        if (changes && 'gridApi' in changes && changes.gridApi.currentValue) {
+            this.applyValues();
+
+            this.gridApi.core.on.rowsVisibleChanged(this.$scope, () => {
+                this.applyValues();
+            });
+        }
+    }
+
+    applyValues() {
+        if (!this.gridApi.grid.rows.length) {
+            this.noData = true;
+
+            return;
+        }
+
+        this.noData = false;
+
+        this.noDataFiltered = _.sumBy(this.gridApi.grid.rows, 'visible') === 0;
+    }
+}
diff --git a/modules/frontend/app/components/grid-no-data/index.js b/modules/frontend/app/components/grid-no-data/index.js
new file mode 100644
index 0000000..2acecf9
--- /dev/null
+++ b/modules/frontend/app/components/grid-no-data/index.js
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import component from './component';
+
+export default angular
+    .module('ignite-console.grid-no-data', [])
+    .component('gridNoData', component);
diff --git a/modules/frontend/app/components/grid-no-data/style.scss b/modules/frontend/app/components/grid-no-data/style.scss
new file mode 100644
index 0000000..0a51ac2
--- /dev/null
+++ b/modules/frontend/app/components/grid-no-data/style.scss
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+grid-no-data {
+    position: relative;
+    display: block;
+    padding: 0 51px;
+
+    border-radius: 0 0 4px 4px;
+    
+    font-style: italic;
+    line-height: 16px;
+
+    [ng-transclude] {
+    	padding: 16px 0;
+    }
+}
diff --git a/modules/frontend/app/components/grid-showing-rows/component.js b/modules/frontend/app/components/grid-showing-rows/component.js
new file mode 100644
index 0000000..ecb93d0
--- /dev/null
+++ b/modules/frontend/app/components/grid-showing-rows/component.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './style.scss';
+import controller from './controller';
+
+import templateUrl from './template.tpl.pug';
+
+export default {
+    templateUrl,
+    controller,
+    bindings: {
+        gridApi: '<'
+    }
+};
diff --git a/modules/frontend/app/components/grid-showing-rows/controller.js b/modules/frontend/app/components/grid-showing-rows/controller.js
new file mode 100644
index 0000000..31d09bb
--- /dev/null
+++ b/modules/frontend/app/components/grid-showing-rows/controller.js
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+export default class {
+    static $inject = ['$scope', 'IgniteCopyToClipboard', 'uiGridExporterService', 'uiGridExporterConstants', 'IgniteMessages', 'CSV'];
+
+    constructor($scope, IgniteCopyToClipboard, uiGridExporterService, uiGridExporterConstants, IgniteMessages, CSV) {
+        Object.assign(this, {$scope, IgniteCopyToClipboard, uiGridExporterService, uiGridExporterConstants, IgniteMessages, CSV});
+
+        this.count = 0;
+        this.visible = 0;
+        this.selected = 0;
+    }
+
+    $onChanges(changes) {
+        if (changes && 'gridApi' in changes && changes.gridApi.currentValue) {
+            this.applyValues();
+
+            this.gridApi.core.on.rowsVisibleChanged(this.$scope, () => {
+                this.applyValues();
+            });
+
+            if (this.gridApi.selection) {
+                this.gridApi.selection.on.rowSelectionChanged(this.$scope, () => this.updateSelectedCount());
+                this.gridApi.selection.on.rowSelectionChangedBatch(this.$scope, () => this.updateSelectedCount());
+            }
+        }
+    }
+
+    updateSelectedCount() {
+        if (!this.gridApi.selection)
+            return;
+
+        this.selected = this.gridApi.selection.getSelectedCount();
+    }
+
+    applyValues() {
+        if (!this.gridApi.grid.rows.length) {
+            this.count = 0;
+            this.visible = 0;
+            this.selected = 0;
+            return;
+        }
+
+        this.count = this.gridApi.grid.rows.length;
+        this.visible = _.sumBy(this.gridApi.grid.rows, (row) => Number(row.visible));
+        this.updateSelectedCount();
+    }
+
+    copyToClipBoard() {
+        if (this.count === 0 || !this.gridApi) {
+            this.IgniteMessages.showError('No data to be copied');
+            return;
+        }
+
+        const data = [];
+        const grid = this.gridApi.grid;
+        grid.options.exporterSuppressColumns = [];
+        const exportColumnHeaders = this.uiGridExporterService.getColumnHeaders(grid, this.uiGridExporterConstants.VISIBLE);
+
+        grid.rows.forEach((row) => {
+            if (!row.visible)
+                return;
+
+            const values = [];
+
+            exportColumnHeaders.forEach((exportCol) => {
+                const col = grid.columns.find(({ field }) => field === exportCol.name);
+
+                if (!col || !col.visible || col.colDef.exporterSuppressExport === true)
+                    return;
+
+                const value = grid.getCellValue(row, col);
+
+                values.push({ value });
+            });
+
+            data.push(values);
+        });
+
+        const csvContent = this.uiGridExporterService.formatAsCsv(exportColumnHeaders, data, this.CSV.getSeparator());
+
+        this.IgniteCopyToClipboard.copy(csvContent);
+    }
+}
diff --git a/modules/frontend/app/components/grid-showing-rows/index.js b/modules/frontend/app/components/grid-showing-rows/index.js
new file mode 100644
index 0000000..9f8c921
--- /dev/null
+++ b/modules/frontend/app/components/grid-showing-rows/index.js
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import component from './component';
+
+export default angular
+    .module('ignite-console.grid-showing-rows', [])
+    .component('gridShowingRows', component);
diff --git a/modules/frontend/app/components/grid-showing-rows/style.scss b/modules/frontend/app/components/grid-showing-rows/style.scss
new file mode 100644
index 0000000..5893a08
--- /dev/null
+++ b/modules/frontend/app/components/grid-showing-rows/style.scss
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+grid-showing-rows {
+	color: #757575;
+
+	i {
+		margin-right: 15px;
+	}
+}
diff --git a/modules/frontend/app/components/grid-showing-rows/template.tpl.pug b/modules/frontend/app/components/grid-showing-rows/template.tpl.pug
new file mode 100644
index 0000000..bce9c86
--- /dev/null
+++ b/modules/frontend/app/components/grid-showing-rows/template.tpl.pug
@@ -0,0 +1,24 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+
+i Total: {{$ctrl.count}}
+
+i Showing: {{$ctrl.visible}}
+
+i Selected: {{$ctrl.selected}} of {{$ctrl.count}}
+
+a(ng-click='$ctrl.copyToClipBoard()') #[i Copy to clipboard]
diff --git a/modules/frontend/app/components/ignite-chart-series-selector/component.js b/modules/frontend/app/components/ignite-chart-series-selector/component.js
new file mode 100644
index 0000000..0ca5402
--- /dev/null
+++ b/modules/frontend/app/components/ignite-chart-series-selector/component.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+
+export default {
+    template,
+    controller,
+    transclude: true,
+    bindings: {
+        chartApi: '<'
+    }
+};
diff --git a/modules/frontend/app/components/ignite-chart-series-selector/controller.js b/modules/frontend/app/components/ignite-chart-series-selector/controller.js
new file mode 100644
index 0000000..f46d8da
--- /dev/null
+++ b/modules/frontend/app/components/ignite-chart-series-selector/controller.js
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class IgniteChartSeriesSelectorController {
+    constructor() {
+        this.charts = [];
+        this.selectedCharts = [];
+    }
+
+    $onChanges(changes) {
+        if (changes && 'chartApi' in changes && changes.chartApi.currentValue) {
+            this.applyValues();
+            this.setSelectedCharts();
+        }
+    }
+
+    applyValues() {
+        this.charts = this._makeMenu();
+        this.selectedCharts = this.charts.filter((chart) => !chart.hidden).map(({ key }) => key);
+    }
+
+    setSelectedCharts() {
+        const selectedDataset = ({ label }) => this.selectedCharts.includes(label);
+
+        this.chartApi.config.data.datasets
+            .forEach((dataset) => {
+                dataset.hidden = true;
+
+                if (!selectedDataset(dataset))
+                    return;
+
+                dataset.hidden = false;
+            });
+
+        this.chartApi.update();
+    }
+
+    _makeMenu() {
+        const labels = this.chartApi.config.datasetLegendMapping;
+
+        return Object.keys(this.chartApi.config.datasetLegendMapping).map((key) => {
+            return {
+                key,
+                label: labels[key].name || labels[key],
+                hidden: labels[key].hidden
+            };
+        });
+    }
+}
diff --git a/modules/frontend/app/components/ignite-chart-series-selector/index.js b/modules/frontend/app/components/ignite-chart-series-selector/index.js
new file mode 100644
index 0000000..7ad3da0
--- /dev/null
+++ b/modules/frontend/app/components/ignite-chart-series-selector/index.js
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import component from './component';
+
+export default angular
+    .module('ignite-console.ignite-chart-series-selector', [])
+    .component('igniteChartSeriesSelector', component);
diff --git a/modules/frontend/app/components/ignite-chart-series-selector/template.pug b/modules/frontend/app/components/ignite-chart-series-selector/template.pug
new file mode 100644
index 0000000..203f12f
--- /dev/null
+++ b/modules/frontend/app/components/ignite-chart-series-selector/template.pug
@@ -0,0 +1,29 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+button.btn-ignite.btn-ignite--link-dashed-secondary(
+    protect-from-bs-select-render
+    bs-select
+    ng-model='$ctrl.selectedCharts'
+    ng-change='$ctrl.setSelectedCharts()'
+    ng-model-options='{debounce: {default: 5}}',
+    bs-options='chart.key as chart.label for chart in $ctrl.charts'
+    bs-on-before-show='$ctrl.onShow'
+    data-multiple='true'
+    ng-transclude
+    ng-disabled='!($ctrl.charts.length)'
+)
+    svg(ignite-icon='gear').icon
diff --git a/modules/frontend/app/components/ignite-chart/component.ts b/modules/frontend/app/components/ignite-chart/component.ts
new file mode 100644
index 0000000..ebe9fdf
--- /dev/null
+++ b/modules/frontend/app/components/ignite-chart/component.ts
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {IgniteChartController} from './controller';
+import templateUrl from './template.tpl.pug';
+
+export default {
+    controller: IgniteChartController,
+    templateUrl,
+    bindings: {
+        chartOptions: '<',
+        chartDataPoint: '<',
+        chartHistory: '<',
+        chartTitle: '<',
+        chartColors: '<',
+        chartHeaderText: '<',
+        refreshRate: '<',
+        resultDataStatus: '<?'
+    },
+    transclude: true
+};
diff --git a/modules/frontend/app/components/ignite-chart/components/chart-no-data/component.ts b/modules/frontend/app/components/ignite-chart/components/chart-no-data/component.ts
new file mode 100644
index 0000000..9627940
--- /dev/null
+++ b/modules/frontend/app/components/ignite-chart/components/chart-no-data/component.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import IgniteChartNoDataCtrl from './controller';
+import templateUrl from './template.tpl.pug';
+
+export default {
+    controller: IgniteChartNoDataCtrl,
+    templateUrl,
+    require: {
+        igniteChart: '^igniteChart'
+    },
+    bindings: {
+        resultDataStatus: '<',
+        handleClusterInactive: '<'
+    }
+} as ng.IComponentOptions;
diff --git a/modules/frontend/app/components/ignite-chart/components/chart-no-data/controller.ts b/modules/frontend/app/components/ignite-chart/components/chart-no-data/controller.ts
new file mode 100644
index 0000000..525c62c
--- /dev/null
+++ b/modules/frontend/app/components/ignite-chart/components/chart-no-data/controller.ts
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {merge} from 'rxjs';
+import {distinctUntilChanged, pluck, tap} from 'rxjs/operators';
+
+import {WellKnownOperationStatus} from 'app/types';
+import {IgniteChartController} from '../../controller';
+
+const BLANK_STATUS = new Set([WellKnownOperationStatus.ERROR, WellKnownOperationStatus.WAITING]);
+
+export default class IgniteChartNoDataCtrl implements ng.IOnChanges, ng.IOnDestroy {
+    static $inject = ['AgentManager'];
+
+    constructor(private AgentManager) {}
+
+    igniteChart: IgniteChartController;
+
+    handleClusterInactive: boolean;
+
+    connectionState$ = this.AgentManager.connectionSbj.pipe(
+        pluck('state'),
+        distinctUntilChanged(),
+        tap((state) => {
+            if (state === 'AGENT_DISCONNECTED')
+                this.destroyChart();
+        })
+    );
+
+    cluster$ = this.AgentManager.connectionSbj.pipe(
+        pluck('cluster'),
+        distinctUntilChanged(),
+        tap((cluster) => {
+            if (!cluster && !this.AgentManager.isDemoMode()) {
+                this.destroyChart();
+                return;
+            }
+
+            if (!!cluster && cluster.active === false && this.handleClusterInactive)
+                this.destroyChart();
+
+        })
+    );
+
+    subsribers$ = merge(
+        this.connectionState$,
+        this.cluster$
+    ).subscribe();
+
+    $onChanges(changes) {
+        if (changes.resultDataStatus && BLANK_STATUS.has(changes.resultDataStatus.currentValue))
+            this.destroyChart();
+    }
+
+    $onDestroy() {
+        this.subsribers$.unsubscribe();
+    }
+
+    destroyChart() {
+        if (this.igniteChart && this.igniteChart.chart) {
+            this.igniteChart.chart.destroy();
+            this.igniteChart.config = null;
+            this.igniteChart.chart = null;
+        }
+    }
+}
diff --git a/modules/frontend/app/components/ignite-chart/components/chart-no-data/index.ts b/modules/frontend/app/components/ignite-chart/components/chart-no-data/index.ts
new file mode 100644
index 0000000..d36668f
--- /dev/null
+++ b/modules/frontend/app/components/ignite-chart/components/chart-no-data/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import IgniteChartNoDataCmp from './component';
+
+export default angular.module('ignite-console.ignite-chart.chart-no-data', [])
+    .component('chartNoData', IgniteChartNoDataCmp);
diff --git a/modules/frontend/app/components/ignite-chart/components/chart-no-data/template.tpl.pug b/modules/frontend/app/components/ignite-chart/components/chart-no-data/template.tpl.pug
new file mode 100644
index 0000000..bbf2598
--- /dev/null
+++ b/modules/frontend/app/components/ignite-chart/components/chart-no-data/template.tpl.pug
@@ -0,0 +1,20 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+no-data(result-data-status='$ctrl.resultDataStatus' handle-cluster-inactive='$ctrl.handleClusterInactive').no-data
+    div(ng-if='!$ctrl.igniteChart.config.data.datasets')
+        | No Data #[br]
+        | Make sure you are connected to the right grid.
diff --git a/modules/frontend/app/components/ignite-chart/controller.js b/modules/frontend/app/components/ignite-chart/controller.js
new file mode 100644
index 0000000..39707b3
--- /dev/null
+++ b/modules/frontend/app/components/ignite-chart/controller.js
@@ -0,0 +1,397 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+import moment from 'moment';
+
+/**
+ * @typedef {{x: number, y: {[key: string]: number}}} IgniteChartDataPoint
+ */
+
+const RANGE_RATE_PRESET = [
+    {label: '1 min', value: 1},
+    {label: '5 min', value: 5},
+    {label: '10 min', value: 10},
+    {label: '15 min', value: 15},
+    {label: '30 min', value: 30}
+];
+
+/**
+ * Determines what label format was chosen by determineLabelFormat function
+ * in Chart.js streaming plugin.
+ * 
+ * @param {string} label
+ */
+const inferLabelFormat = (label) => {
+    if (label.match(/\.\d{3} (am|pm)$/)) return 'MMM D, YYYY h:mm:ss.SSS a';
+    if (label.match(/:\d{1,2} (am|pm)$/)) return 'MMM D, YYYY h:mm:ss a';
+    if (label.match(/ \d{4}$/)) return 'MMM D, YYYY';
+};
+
+export class IgniteChartController {
+    /** @type {import('chart.js').ChartConfiguration} */
+    chartOptions;
+    /** @type {string} */
+    chartTitle;
+    /** @type {IgniteChartDataPoint} */
+    chartDataPoint;
+    /** @type {Array<IgniteChartDataPoint>} */
+    chartHistory;
+    newPoints = [];
+
+    static $inject = ['$element', 'IgniteChartColors', '$filter'];
+
+    /**
+     * @param {JQLite} $element
+     * @param {Array<string>} IgniteChartColors
+     * @param {ng.IFilterService} $filter
+     */
+    constructor($element, IgniteChartColors, $filter) {
+        this.$element = $element;
+        this.IgniteChartColors = IgniteChartColors;
+
+        this.datePipe = $filter('date');
+        this.ranges = RANGE_RATE_PRESET;
+        this.currentRange = this.ranges[0];
+        this.maxRangeInMilliseconds = RANGE_RATE_PRESET[RANGE_RATE_PRESET.length - 1].value * 60 * 1000;
+        this.ctx = this.$element.find('canvas')[0].getContext('2d');
+
+        this.localHistory = [];
+        this.updateIsBusy = false;
+    }
+
+    $onDestroy() {
+        if (this.chart)
+            this.chart.destroy();
+
+        this.$element = this.ctx = this.chart = null;
+    }
+
+    $onInit() {
+        this.chartColors = _.get(this.chartOptions, 'chartColors', this.IgniteChartColors);
+    }
+
+    _refresh() {
+        this.onRefresh();
+        this.rerenderChart();
+    }
+
+    /**
+     * @param {{chartOptions: ng.IChangesObject<import('chart.js').ChartConfiguration>, chartTitle: ng.IChangesObject<string>, chartDataPoint: ng.IChangesObject<IgniteChartDataPoint>, chartHistory: ng.IChangesObject<Array<IgniteChartDataPoint>>}} changes
+     */
+    async $onChanges(changes) {
+        if (this.chart && _.get(changes, 'refreshRate.currentValue'))
+            this.onRefreshRateChanged(_.get(changes, 'refreshRate.currentValue'));
+
+        if ((changes.chartDataPoint && _.isNil(changes.chartDataPoint.currentValue)) ||
+            (changes.chartHistory && _.isEmpty(changes.chartHistory.currentValue))) {
+            this.clearDatasets();
+            this.localHistory = [];
+
+            return;
+        }
+
+        if (changes.chartHistory && changes.chartHistory.currentValue && changes.chartHistory.currentValue.length !== changes.chartHistory.previousValue.length) {
+            if (!this.chart)
+                await this.initChart();
+
+            this.clearDatasets();
+            this.localHistory = [...changes.chartHistory.currentValue];
+
+            this.newPoints.splice(0, this.newPoints.length, ...changes.chartHistory.currentValue);
+
+            this._refresh();
+
+            return;
+        }
+
+        if (this.chartDataPoint && changes.chartDataPoint) {
+            if (!this.chart)
+                this.initChart();
+
+            this.newPoints.push(this.chartDataPoint);
+            this.localHistory.push(this.chartDataPoint);
+
+            this._refresh();
+        }
+    }
+
+    async initChart() {
+        /** @type {import('chart.js').ChartConfiguration} */
+        this.config = {
+            type: 'LineWithVerticalCursor',
+            data: {
+                datasets: []
+            },
+            options: {
+                elements: {
+                    line: {
+                        tension: 0
+                    },
+                    point: {
+                        radius: 2,
+                        pointStyle: 'rectRounded'
+                    }
+                },
+                animation: {
+                    duration: 0 // general animation time
+                },
+                hover: {
+                    animationDuration: 0 // duration of animations when hovering an item
+                },
+                responsiveAnimationDuration: 0, // animation duration after a resize
+                maintainAspectRatio: false,
+                responsive: true,
+                legend: {
+                    display: false
+                },
+                scales: {
+                    xAxes: [{
+                        type: 'realtime',
+                        display: true,
+                        time: {
+                            displayFormats: {
+                                second: 'HH:mm:ss',
+                                minute: 'HH:mm:ss',
+                                hour: 'HH:mm:ss'
+                            }
+                        },
+                        ticks: {
+                            maxRotation: 0,
+                            minRotation: 0
+                        }
+                    }],
+                    yAxes: [{
+                        type: 'linear',
+                        display: true,
+                        ticks: {
+                            min: 0,
+                            beginAtZero: true,
+                            maxTicksLimit: 4,
+                            callback: (value, index, labels) => {
+                                if (value === 0)
+                                    return 0;
+
+                                if (_.max(labels) <= 4000 && value <= 4000)
+                                    return value;
+
+                                if (_.max(labels) <= 1000000 && value <= 1000000)
+                                    return `${value / 1000}K`;
+
+                                if ((_.max(labels) <= 4000000 && value >= 500000) || (_.max(labels) > 4000000))
+                                    return `${value / 1000000}M`;
+
+                                return value;
+                            }
+                        }
+                    }]
+                },
+                tooltips: {
+                    mode: 'index',
+                    position: 'yCenter',
+                    intersect: false,
+                    yAlign: 'center',
+                    xPadding: 20,
+                    yPadding: 20,
+                    bodyFontSize: 13,
+                    callbacks: {
+                        title: (tooltipItem) => {
+                            return tooltipItem[0].xLabel = moment(tooltipItem[0].xLabel, inferLabelFormat(tooltipItem[0].xLabel)).format('HH:mm:ss');
+                        },
+                        label: (tooltipItem, data) => {
+                            const label = data.datasets[tooltipItem.datasetIndex].label || '';
+
+                            return `${_.startCase(label)}: ${tooltipItem.yLabel} per sec`;
+                        },
+                        labelColor: (tooltipItem) => {
+                            return {
+                                borderColor: 'rgba(255,255,255,0.5)',
+                                borderWidth: 0,
+                                boxShadow: 'none',
+                                backgroundColor: this.chartColors[tooltipItem.datasetIndex]
+                            };
+                        }
+                    }
+                },
+                plugins: {
+                    streaming: {
+                        duration: this.currentRange.value * 1000 * 60,
+                        frameRate: 1000 / this.refreshRate || 1 / 3,
+                        refresh: this.refreshRate || 3000,
+                        // Temporary workaround before https://github.com/nagix/chartjs-plugin-streaming/issues/53 resolved.
+                        // ttl: this.maxRangeInMilliseconds,
+                        onRefresh: () => {
+                            this.onRefresh();
+                        }
+                    }
+                }
+            }
+        };
+
+        this.config = _.merge(this.config, this.chartOptions);
+
+        const chartModule = await import('chart.js');
+        const Chart = chartModule.default;
+
+        Chart.Tooltip.positioners.yCenter = (elements) => {
+            const chartHeight = elements[0]._chart.height;
+            const tooltipHeight = 60;
+
+            return {x: elements[0].getCenterPoint().x, y: Math.floor(chartHeight / 2) - Math.floor(tooltipHeight / 2) };
+        };
+
+
+        // Drawing vertical cursor
+        Chart.defaults.LineWithVerticalCursor = Chart.defaults.line;
+        Chart.controllers.LineWithVerticalCursor = Chart.controllers.line.extend({
+            draw(ease) {
+                Chart.controllers.line.prototype.draw.call(this, ease);
+
+                if (this.chart.tooltip._active && this.chart.tooltip._active.length) {
+                    const activePoint = this.chart.tooltip._active[0];
+                    const ctx = this.chart.ctx;
+                    const x = activePoint.tooltipPosition().x;
+                    const topY = this.chart.scales['y-axis-0'].top;
+                    const bottomY = this.chart.scales['y-axis-0'].bottom;
+
+                    // draw line
+                    ctx.save();
+                    ctx.beginPath();
+                    ctx.moveTo(x, topY);
+                    ctx.lineTo(x, bottomY);
+                    ctx.lineWidth = 0.5;
+                    ctx.strokeStyle = '#0080ff';
+                    ctx.stroke();
+                    ctx.restore();
+                }
+            }
+        });
+
+        await import('chartjs-plugin-streaming');
+
+        this.chart = new Chart(this.ctx, this.config);
+        this.changeXRange(this.currentRange);
+    }
+
+    onRefresh() {
+        this.newPoints.forEach((point) => {
+            this.appendChartPoint(point);
+        });
+
+        this.newPoints.splice(0, this.newPoints.length);
+    }
+
+    /**
+     * @param {IgniteChartDataPoint} dataPoint
+     */
+    appendChartPoint(dataPoint) {
+        Object.keys(dataPoint.y).forEach((key) => {
+            if (this.checkDatasetCanBeAdded(key)) {
+                let datasetIndex = this.findDatasetIndex(key);
+
+                if (datasetIndex < 0) {
+                    datasetIndex = this.config.data.datasets.length;
+                    this.addDataset(key);
+                }
+
+                this.config.data.datasets[datasetIndex].data.push({x: dataPoint.x, y: dataPoint.y[key]});
+                this.config.data.datasets[datasetIndex].borderColor = this.chartColors[datasetIndex];
+                this.config.data.datasets[datasetIndex].borderWidth = 2;
+                this.config.data.datasets[datasetIndex].fill = false;
+            }
+        });
+
+        // Temporary workaround before https://github.com/nagix/chartjs-plugin-streaming/issues/53 resolved.
+        this.pruneHistory();
+    }
+
+    // Temporary workaround before https://github.com/nagix/chartjs-plugin-streaming/issues/53 resolved.
+    pruneHistory() {
+        if (!this.xRangeUpdateInProgress) {
+            const currenTime = Date.now();
+
+            while (currenTime - this.localHistory[0].x > this.maxRangeInMilliseconds)
+                this.localHistory.shift();
+
+            this.config.data.datasets.forEach((dataset) => {
+                while (currenTime - dataset.data[0].x > this.maxRangeInMilliseconds)
+                    dataset.data.shift();
+            });
+        }
+    }
+
+    /**
+     * Checks if a key of dataset can be added to chart or should be ignored.
+     * @param dataPointKey {String}
+     * @return {Boolean}
+     */
+    checkDatasetCanBeAdded(dataPointKey) {
+        // If datasetLegendMapping is empty all keys are allowed.
+        if (!this.config.datasetLegendMapping)
+            return true;
+
+        return Object.keys(this.config.datasetLegendMapping).includes(dataPointKey);
+    }
+
+    clearDatasets() {
+        if (!_.isNil(this.config))
+            this.config.data.datasets.forEach((dataset) => dataset.data = []);
+    }
+
+    addDataset(datasetName) {
+        if (this.findDatasetIndex(datasetName) >= 0)
+            throw new Error(`Dataset with name ${datasetName} is already in chart`);
+        else {
+            const datasetIsHidden = _.isNil(this.config.datasetLegendMapping[datasetName].hidden)
+                ? false
+                : this.config.datasetLegendMapping[datasetName].hidden;
+
+            this.config.data.datasets.push({ label: datasetName, data: [], hidden: datasetIsHidden });
+        }
+    }
+
+    findDatasetIndex(searchedDatasetLabel) {
+        return this.config.data.datasets.findIndex((dataset) => dataset.label === searchedDatasetLabel);
+    }
+
+    changeXRange(range) {
+        if (this.chart) {
+            this.xRangeUpdateInProgress = true;
+
+            this.chart.config.options.plugins.streaming.duration = range.value * 60 * 1000;
+
+            this.clearDatasets();
+            this.newPoints.splice(0, this.newPoints.length, ...this.localHistory);
+
+            this.onRefresh();
+            this.rerenderChart();
+
+            this.xRangeUpdateInProgress = false;
+        }
+    }
+
+    onRefreshRateChanged(refreshRate) {
+        this.chart.config.options.plugins.streaming.frameRate = 1000 / refreshRate;
+        this.chart.config.options.plugins.streaming.refresh = refreshRate;
+        this.rerenderChart();
+    }
+
+    rerenderChart() {
+        if (this.chart)
+            this.chart.update();
+    }
+}
diff --git a/modules/frontend/app/components/ignite-chart/index.js b/modules/frontend/app/components/ignite-chart/index.js
new file mode 100644
index 0000000..957a2ae
--- /dev/null
+++ b/modules/frontend/app/components/ignite-chart/index.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import chartNoData from './components/chart-no-data';
+import IgniteChartCmp from './component';
+import './style.scss';
+
+export default angular
+    .module('ignite-console.ignite-chart', [chartNoData.name])
+    .component('igniteChart', IgniteChartCmp);
diff --git a/modules/frontend/app/components/ignite-chart/style.scss b/modules/frontend/app/components/ignite-chart/style.scss
new file mode 100644
index 0000000..3a07bd5
--- /dev/null
+++ b/modules/frontend/app/components/ignite-chart/style.scss
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ignite-chart {
+  display: flex;
+  flex-direction: column;
+
+  height: 100%;
+
+  header {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    box-sizing: content-box;
+
+    height: 36px;
+    min-height: 36px;
+    padding: 7px 21px;
+
+    border-bottom: 1px solid #d4d4d4;
+
+    h5 {
+      margin: 0;
+
+      font-size: 16px;
+      font-weight: normal;
+      line-height: 36px;
+    }
+
+    ignite-chart-series-selector {
+      margin: 0 2px;
+    }
+
+    > div {
+      &:first-child {
+        width: calc(100% - 120px);
+        white-space: nowrap;
+      }
+
+      display: flex;
+      align-items: center;
+      flex-wrap: nowrap;
+      flex-grow: 0;
+
+      .chart-text {
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+    }
+  }
+
+  .ignite-chart-placeholder {
+    display: block;
+    height: calc(100% - 71px);
+    margin-top: 20px;
+  }
+
+  .no-data {
+    position: absolute;
+    top: 50%;
+
+    width: 100%;
+    padding: 0 20px;
+
+    border-radius: 0 0 4px 4px;
+
+    font-style: italic;
+    line-height: 16px;
+    text-align: center;
+  }
+}
diff --git a/modules/frontend/app/components/ignite-chart/template.tpl.pug b/modules/frontend/app/components/ignite-chart/template.tpl.pug
new file mode 100644
index 0000000..bfc4c69
--- /dev/null
+++ b/modules/frontend/app/components/ignite-chart/template.tpl.pug
@@ -0,0 +1,35 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+header
+    div
+        h5 {{ $ctrl.chartTitle }}
+        ignite-chart-series-selector(chart-api='$ctrl.chart')
+        .chart-text(ng-if='$ctrl.chartHeaderText') {{$ctrl.chartHeaderText}}
+
+    div
+        span Range:
+            button.btn-ignite.btn-ignite--link-success.link-primary(
+                ng-model='$ctrl.currentRange'
+                ng-change='$ctrl.changeXRange($ctrl.currentRange)'
+                bs-options='item as item.label for item in $ctrl.ranges'
+                bs-select
+            )
+
+.ignite-chart-placeholder
+    canvas
+
+ng-transclude
\ No newline at end of file
diff --git a/modules/frontend/app/components/ignite-icon/directive.js b/modules/frontend/app/components/ignite-icon/directive.js
new file mode 100644
index 0000000..9838fe2
--- /dev/null
+++ b/modules/frontend/app/components/ignite-icon/directive.js
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default function() {
+    return {
+        restrict: 'A',
+        controller: class {
+            static $inject = ['$scope', '$attrs', '$sce', '$element', '$window', 'IgniteIcon'];
+
+            /**
+             * @param {ng.IScope} $scope     
+             * @param {ng.IAttributes} $attrs     
+             * @param {ng.ISCEService} $sce       
+             * @param {JQLite} $element   
+             * @param {ng.IWindowService} $window    
+             * @param {import('./service').default} IgniteIcon 
+             */
+            constructor($scope, $attrs, $sce, $element, $window, IgniteIcon) {
+                this.$scope = $scope;
+                this.$attrs = $attrs;
+                this.$sce = $sce;
+                this.$element = $element;
+                this.$window = $window;
+                this.IgniteIcon = IgniteIcon;
+            }
+
+            $onInit() {
+                this.off = this.$scope.$on('$locationChangeSuccess', (e, url) => {
+                    this.render(this.getFragmentURL(url));
+                });
+
+                this.wrapper = document.createElement('div');
+            }
+
+            $onDestroy() {
+                this.$element = this.$window = this.wrapper = null;
+
+                this.off();
+            }
+
+            $postLink() {
+                /** @type {string} */
+                this.name = this.$attrs.igniteIcon;
+                this.$element.attr('viewBox', this.IgniteIcon.getIcon(this.name).viewBox);
+
+                this.render(this.getFragmentURL());
+            }
+
+            getFragmentURL(url = this.$window.location.href) {
+                // All browsers except for Chrome require absolute URL of a fragment.
+                // Combine that with base tag and HTML5 navigation mode and you get this.
+                return `${url.split('#')[0]}#${this.name}`;
+            }
+
+            /**
+             * @param {string} url 
+             */
+            render(url) {
+                // templateNamespace: 'svg' does not work in IE11
+                this.wrapper.innerHTML = `<svg><use xlink:href="${url}" href="${url}" /></svg>`;
+
+                Array.from(this.wrapper.childNodes[0].childNodes).forEach((n) => {
+                    this.$element.empty().append(n);
+                });
+            }
+        }
+    };
+}
diff --git a/modules/frontend/app/components/ignite-icon/index.js b/modules/frontend/app/components/ignite-icon/index.js
new file mode 100644
index 0000000..30954f1
--- /dev/null
+++ b/modules/frontend/app/components/ignite-icon/index.js
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import directive from './directive';
+import service from './service';
+import './style.scss';
+
+export default angular
+    .module('ignite-console.ignite-icon', [])
+    .service('IgniteIcon', service)
+    .directive('igniteIcon', directive);
diff --git a/modules/frontend/app/components/ignite-icon/service.js b/modules/frontend/app/components/ignite-icon/service.js
new file mode 100644
index 0000000..34ea074
--- /dev/null
+++ b/modules/frontend/app/components/ignite-icon/service.js
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @typedef {{id: string, viewBox: string, content: string}} SpriteSymbol
+ */
+
+/**
+ * @typedef {{[name: string]: SpriteSymbol}} Icons
+ */
+
+export default class IgniteIcon {
+    /**
+     * @type {Icons}
+     */
+    _icons = {};
+
+    /**
+     * @param {Icons} icons
+     */
+    registerIcons(icons) {
+        return Object.assign(this._icons, icons);
+    }
+
+    /**
+     * @param {string} name
+     */
+    getIcon(name) {
+        return this._icons[name];
+    }
+
+    getAllIcons() {
+        return this._icons;
+    }
+}
diff --git a/modules/frontend/app/components/ignite-icon/style.scss b/modules/frontend/app/components/ignite-icon/style.scss
new file mode 100644
index 0000000..5bff0fb
--- /dev/null
+++ b/modules/frontend/app/components/ignite-icon/style.scss
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+[ignite-icon] {
+    height: 16px;
+    width: 16px;
+}
+
+[ignite-icon='expand'],
+[ignite-icon='collapse'] {
+    width: 13px;
+    height: 13px;
+}
diff --git a/modules/frontend/app/components/input-dialog/index.js b/modules/frontend/app/components/input-dialog/index.js
new file mode 100644
index 0000000..4bb9642
--- /dev/null
+++ b/modules/frontend/app/components/input-dialog/index.js
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import inputDialog from './input-dialog.service';
+
+angular
+    .module('ignite-console.input-dialog', [])
+    .service('IgniteInput', inputDialog);
diff --git a/modules/frontend/app/components/input-dialog/input-dialog.controller.js b/modules/frontend/app/components/input-dialog/input-dialog.controller.js
new file mode 100644
index 0000000..1de142c
--- /dev/null
+++ b/modules/frontend/app/components/input-dialog/input-dialog.controller.js
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+export default class InputDialogController {
+    static $inject = ['deferred', 'ui'];
+
+    constructor(deferred, options) {
+        this.deferred = deferred;
+        this.options = options;
+    }
+
+    confirm() {
+        if (_.isFunction(this.options.toValidValue))
+            return this.deferred.resolve(this.options.toValidValue(this.options.value));
+
+        this.deferred.resolve(this.options.value);
+    }
+}
diff --git a/modules/frontend/app/components/input-dialog/input-dialog.service.ts b/modules/frontend/app/components/input-dialog/input-dialog.service.ts
new file mode 100644
index 0000000..b2a241f
--- /dev/null
+++ b/modules/frontend/app/components/input-dialog/input-dialog.service.ts
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+import controller from './input-dialog.controller';
+import templateUrl from './input-dialog.tpl.pug';
+import {CancellationError} from 'app/errors/CancellationError';
+
+type InputModes = 'text' | 'number' | 'email' | 'date' | 'time' | 'date-and-time';
+
+interface ValidationFunction<T> {
+    (value: T): boolean
+}
+
+/**
+ * Options for rendering inputs.
+ */
+interface InputOptions<T> {
+    /** Input type. */
+    mode?: InputModes,
+    /** Dialog title. */
+    title?: string,
+    /** Input field label. */
+    label?: string,
+    /** Message for tooltip in label. */
+    tip?: string,
+    /** Default value. */
+    value: T,
+    /** Placeholder for input. */
+    placeholder?: string,
+    /** Validator function. */
+    toValidValue?: ValidationFunction<T>,
+    /** Min value for number input. */
+    min?: number,
+    /** Max value for number input. */
+    max?: number,
+    /** Postfix for units in number input. */
+    postfix?: string
+}
+
+export default class InputDialog {
+    static $inject = ['$modal', '$q'];
+
+    constructor(private $modal: mgcrea.ngStrap.modal.IModalService, private $q: ng.IQService) {}
+
+    /**
+     * Fabric for creating modal instance with different input types.
+     *
+     * @returns User input.
+     */
+    private dialogFabric<T>(args: InputOptions<T>) {
+        const deferred = this.$q.defer<T>();
+
+        const modal = this.$modal({
+            templateUrl,
+            resolve: {
+                deferred: () => deferred,
+                ui: () => args
+            },
+            controller,
+            controllerAs: 'ctrl'
+        });
+
+        const modalHide = modal.hide;
+
+        modal.hide = () => deferred.reject(new CancellationError());
+
+        return deferred.promise
+            .finally(modalHide);
+    }
+
+    /**
+     * Open input dialog to configure custom value.
+     *
+     * @param title Dialog title.
+     * @param label Input field label.
+     * @param value Default value.
+     * @param toValidValue Validator function.
+     * @param mode Input type.
+     */
+    input<T>(title: string, label: string, value: T, toValidValue?: ValidationFunction<T>, mode: InputModes = 'text') {
+        return this.dialogFabric<T>({title, label, value, toValidValue, mode});
+    }
+
+    /**
+     * Open input dialog to configure cloned object name.
+     *
+     * @param srcName Name of source object.
+     * @param names List of already exist names.
+     * @returns New name.
+     */
+    clone(srcName: string, names: Array<string>) {
+        const uniqueName = (value) => {
+            let num = 1;
+            let tmpName = value;
+
+            while (_.includes(names, tmpName)) {
+                tmpName = `${value}_${num}`;
+
+                num++;
+            }
+
+            return tmpName;
+        };
+
+        return this.input<string>('Clone', 'New name', uniqueName(srcName), uniqueName);
+    }
+
+    /**
+     * Open input dialog to configure custom number value.
+     *
+     * @param options Object with settings for rendering number input.
+     * @returns User input.
+     */
+    number(options: InputOptions<number>) {
+        return this.dialogFabric({mode: 'number', ...options});
+    }
+
+    /**
+     * Open input dialog to configure custom e-mail.
+     *
+     * @param options Object with settings for rendering e-mail input.
+     * @return User input.
+     */
+    email(options: InputOptions<string>) {
+        return this.dialogFabric({mode: 'email', ...options});
+    }
+
+    /**
+     * Open input dialog to configure custom date value.
+     *
+     * @param options Settings for rendering date input.
+     * @returns User input.
+     */
+    date(options: InputOptions<Date>) {
+        return this.dialogFabric({mode: 'date', ...options});
+    }
+
+    /**
+     * Open input dialog to configure custom time value.
+     *
+     * @param options Settings for rendering time input.
+     * @returns User input.
+     */
+    time(options: InputOptions<Date>) {
+        return this.dialogFabric({mode: 'time', ...options});
+    }
+
+    /**
+     * Open input dialog to configure custom date and time value.
+     *
+     * @param options Settings for rendering date and time inputs.
+     * @returns User input.
+     */
+    dateTime(options: InputOptions<Date>) {
+        return this.dialogFabric({mode: 'date-and-time', ...options});
+    }
+}
diff --git a/modules/frontend/app/components/input-dialog/input-dialog.tpl.pug b/modules/frontend/app/components/input-dialog/input-dialog.tpl.pug
new file mode 100644
index 0000000..23896e7
--- /dev/null
+++ b/modules/frontend/app/components/input-dialog/input-dialog.tpl.pug
@@ -0,0 +1,108 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+.modal.modal--ignite.theme--ignite.input-dialog(tabindex='-1' role='dialog')
+    .modal-dialog
+        form.modal-content(name='ctrl.form' novalidate)
+            .modal-header
+                h4.modal-title
+                    span {{ ctrl.options.title }}
+                button.close(type='button' aria-label='Close' ng-click='$hide()')
+                     svg(ignite-icon="cross")
+
+            .modal-body(ng-switch='ctrl.options.mode')
+                .row(ng-switch-when='text')
+                    .col-100
+                        +form-field__text({
+                            label: '{{ ctrl.options.label }}',
+                            model: 'ctrl.options.value',
+                            name: '"inputDialogField"',
+                            required: true,
+                            placeholder: 'Enter value'
+                        })(
+                            ignite-form-field-input-autofocus='true'
+                            ignite-on-enter='form.$valid && ctrl.confirm()'
+                        )
+
+                .row(ng-switch-when='number')
+                    .col-100
+                        +form-field__number({
+                            label: '{{ ctrl.options.label }}',
+                            model: 'ctrl.options.value',
+                            name: '"number"',
+                            placeholder: '{{ ctrl.options.placeholder }}',
+                            min: '{{ ctrl.options.min }}',
+                            max: '{{ ctrl.options.max  }}',
+                            tip: '{{ ctrl.options.tip  }}',
+                            postfix: '{{ ctrl.options.postfix }}',
+                            required: true
+                        })
+
+                .row(ng-switch-when='email')
+                    .col-100
+                        +form-field__email({
+                            label: '{{ ctrl.options.label }}',
+                            model: 'ctrl.options.value',
+                            name: '"email"',
+                            required: true,
+                            placeholder: 'Input email'
+                        })(
+                            ignite-form-field-input-autofocus='true'
+                            ignite-on-enter='form.$valid && ctrl.confirm()'
+                        )
+
+                .row(ng-switch-when='date')
+                    .col-100
+                        .form-field--inline
+                            +form-field__datepicker({
+                                label: '{{ ctrl.options.label }}',
+                                model: 'ctrl.options.value',
+                                name: '"date"',
+                                minview: 0,
+                                format: 'dd/MM/yyyy'
+                            })
+
+                .row(ng-switch-when='time')
+                    .col-100
+                        .form-field--inline
+                            +form-field__timepicker({
+                                label: '{{ ctrl.options.label }}',
+                                model: 'ctrl.options.value',
+                                name: '"time"'
+                            })
+
+                .row(ng-switch-when='date-and-time')
+                    .col-100
+                        .form-field--inline
+                            +form-field__datepicker({
+                                label: '{{ ctrl.options.label }}',
+                                model: 'ctrl.options.value',
+                                name: '"date"',
+                                minview: 0,
+                                format: 'dd/MM/yyyy'
+                            })
+                        .form-field--inline
+                            +form-field__timepicker({
+                                model: 'ctrl.options.value',
+                                format: '{{ ctrl.options.format }}',
+                                name: '"time"'
+                            })
+            .modal-footer
+                div
+                    button#copy-btn-cancel.btn-ignite.btn-ignite--link-success(ng-click='$hide()') Cancel
+                    button#copy-btn-confirm.btn-ignite.btn-ignite--success(ng-disabled='ctrl.form.$invalid' ng-click='ctrl.confirm()') Confirm
diff --git a/modules/frontend/app/components/list-editable/component.js b/modules/frontend/app/components/list-editable/component.js
new file mode 100644
index 0000000..8cdc083
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/component.js
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+
+import './style.scss';
+
+export default {
+    controller,
+    template,
+    require: {
+        ngModel: '^ngModel'
+    },
+    bindings: {
+    },
+    transclude: {
+        noItems: '?listEditableNoItems',
+        itemView: '?listEditableItemView',
+        itemEdit: '?listEditableItemEdit'
+    }
+};
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/component.spec.js b/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/component.spec.js
new file mode 100644
index 0000000..06c6b84
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/component.spec.js
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'mocha';
+import {assert} from 'chai';
+import {spy} from 'sinon';
+import {ListEditableAddItemButton as Ctrl} from './component';
+
+suite('list-editable-add-item-button component', () => {
+    test('has addItem method with correct locals', () => {
+        const i = new Ctrl();
+        i._listEditable = {
+            ngModel: {
+                editListItem: spy()
+            }
+        };
+        i._listEditable.ngModel.editListItem.bind = spy(() => i._listEditable.ngModel.editListItem);
+        i._addItem = spy();
+        i.addItem();
+        assert.isOk(i._addItem.calledOnce);
+        assert.deepEqual(i._addItem.lastCall.args[0].$edit, i._listEditable.ngModel.editListItem );
+    });
+
+    test('inserts button after list-editable', () => {
+        Ctrl.hasItemsTemplate = 'tpl';
+        const $scope = {};
+        const clone = {
+            insertAfter: spy()
+        };
+        const $transclude = spy((scope, attach) => attach(clone));
+        const $compile = spy(() => $transclude);
+        const i = new Ctrl($compile, $scope);
+        i._listEditable = {
+            ngModel: {
+                editListItem: spy(),
+                $element: {}
+            }
+        };
+        i.$postLink();
+        assert.isOk($compile.calledOnce);
+        assert.equal($compile.lastCall.args[0], Ctrl.hasItemsTemplate);
+        assert.equal($transclude.lastCall.args[0], $scope);
+        assert.equal(clone.insertAfter.lastCall.args[0], i._listEditable.$element);
+    });
+
+    test('exposes hasItems getter', () => {
+        const i = new Ctrl();
+        i._listEditable = {
+            ngModel: {
+                $isEmpty: spy((v) => !v.length),
+                $viewValue: [1, 2, 3]
+            }
+        };
+        assert.isOk(i.hasItems);
+        i._listEditable.ngModel.$viewValue = [];
+        assert.isNotOk(i.hasItems);
+    });
+});
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/component.ts b/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/component.ts
new file mode 100644
index 0000000..793270f
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/component.ts
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {default as ListEditable} from '../../controller';
+import noItemsTemplate from './no-items-template.pug';
+import hasItemsTemplate from './has-items-template.pug';
+import './style.scss';
+
+/**
+ * Adds "add new item" button to list-editable-no-items slot and after list-editable
+ */
+export class ListEditableAddItemButton<T> {
+    /** 
+     * Template for button that's inserted after list-editable
+     */
+    static hasItemsTemplate: string = hasItemsTemplate;
+    _listEditable: ListEditable<T>;
+    labelSingle: string;
+    labelMultiple: string;
+    _addItem: ng.ICompiledExpression;
+
+    static $inject = ['$compile', '$scope'];
+
+    constructor(private $compile: ng.ICompileService, private $scope: ng.IScope) {}
+
+    $onDestroy() {
+        this._listEditable = this._hasItemsButton = null;
+    }
+
+    $postLink() {
+        this.$compile(ListEditableAddItemButton.hasItemsTemplate)(this.$scope, (hasItemsButton) => {
+            hasItemsButton.insertAfter(this._listEditable.$element);
+        });
+    }
+
+    get hasItems() {
+        return !this._listEditable.ngModel.$isEmpty(this._listEditable.ngModel.$viewValue);
+    }
+
+    addItem() {
+        return this._addItem({
+            $edit: this._listEditable.ngModel.editListItem.bind(this._listEditable),
+            $editLast: (length) => this._listEditable.ngModel.editListIndex(length - 1)
+        });
+    }
+}
+
+export default {
+    controller: ListEditableAddItemButton,
+    require: {
+        _listEditable: '^listEditable'
+    },
+    bindings: {
+        _addItem: '&addItem',
+        labelSingle: '@',
+        labelMultiple: '@'
+    },
+    template: noItemsTemplate
+};
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/has-items-template.pug b/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/has-items-template.pug
new file mode 100644
index 0000000..272f487
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/has-items-template.pug
@@ -0,0 +1,23 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+button.btn-ignite.btn-ignite--link(
+    list-editable-add-item-button-has-items-button
+    type='button'
+    ng-if='$ctrl.hasItems'
+    ng-click='$ctrl.addItem()'
+)
+    | + Add new {{::$ctrl.labelSingle}}
\ No newline at end of file
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/index.ts b/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/index.ts
new file mode 100644
index 0000000..f7e3364
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+.module('list-editable.add-item-button', [])
+.directive('listEditableAddItemButtonHasItemsButton', () => (scope, el) => scope.$on('$destroy', () => el.remove()))
+.component('listEditableAddItemButton', component);
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/no-items-template.pug b/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/no-items-template.pug
new file mode 100644
index 0000000..0593795
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/no-items-template.pug
@@ -0,0 +1,18 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+| You have no {{::$ctrl.labelMultiple}}. 
+a.link-success(ng-click=`$ctrl.addItem()`) Create one?
\ No newline at end of file
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/style.scss b/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/style.scss
new file mode 100644
index 0000000..7274e2b
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-add-item-button/style.scss
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+list-editable-add-item-button {
+    font-style: italic;
+}
\ No newline at end of file
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-cols/cols.directive.js b/modules/frontend/app/components/list-editable/components/list-editable-cols/cols.directive.js
new file mode 100644
index 0000000..e166402
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-cols/cols.directive.js
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './cols.template.pug';
+import './cols.style.scss';
+
+/**
+ * A column definition.
+ *
+ * @typedef {Object} IListEditableColDef
+ * @prop {string} [name] - optional name to display at column head
+ * @prop {string} [cellClass] - CSS class to assign to column cells
+ * @prop {string} [tip] - optional tip to display at column head
+ */
+export class ListEditableColsController {
+    /** @type {Array<IListEditableColDef>} - column definitions */
+    colDefs;
+    /** @type {string} - optional class to assign to rows */
+    rowClass;
+    /** @type {ng.INgModelController} */
+    ngModel;
+
+    static $inject = ['$compile', '$element', '$scope'];
+
+    /**
+     * @param {ng.ICompileService} $compile
+     * @param {JQLite} $element
+     * @param {ng.IScope} $scope
+     */
+    constructor($compile, $element, $scope) {
+        this.$compile = $compile;
+        this.$element = $element;
+        this.$scope = $scope;
+    }
+
+    $postLink() {
+        this.$compile(template)(this.$scope.$new(true), (clone, scope) => {
+            scope.$ctrl = this;
+
+            this.$element[0].parentElement.insertBefore(clone[0], this.$element[0]);
+        });
+    }
+
+    $onDestroy() {
+        this.$element = null;
+    }
+}
+
+/** @returns {ng.IDirective} */
+export default function listEditableCols() {
+    return {
+        controller: ListEditableColsController,
+        controllerAs: '$colsCtrl',
+        require: {
+            ngModel: 'ngModel'
+        },
+        bindToController: {
+            colDefs: '<listEditableCols',
+            rowClass: '@?listEditableColsRowClass',
+            ngDisabled: '<?'
+        }
+    };
+}
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-cols/cols.style.scss b/modules/frontend/app/components/list-editable/components/list-editable-cols/cols.style.scss
new file mode 100644
index 0000000..5735be3
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-cols/cols.style.scss
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.list-editable-cols__header {
+    $index-column-width: 46px;
+    $remove-column-width: 36px;
+
+    margin-right: $remove-column-width;
+    transition: 0.2s opacity;
+
+    &__multiple-cols {
+        margin-left: $index-column-width;
+    }
+
+    .form-field__label {
+        padding-left: 0;
+        padding-right: 0;
+        float: none;
+    }
+
+    [ignite-icon='info'] {
+        @import '../../../../../public/stylesheets/variables';
+
+        margin-left: 5px;
+        color: $ignite-brand-success;
+    }
+
+    &+list-editable {
+        .form-field__label,
+        .ignite-form-field__label {
+            display: none;
+        }
+
+        .form-field:not(.form-field__checkbox) {
+            margin-left: -11px;
+        }
+
+        .le-row-item-view:nth-last-child(2) {
+            display: none;
+        }
+    }
+
+    &[disabled] {
+        opacity: 0.5;
+        cursor: default;
+    }
+}
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-cols/cols.template.pug b/modules/frontend/app/components/list-editable/components/list-editable-cols/cols.template.pug
new file mode 100644
index 0000000..f1aff2e
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-cols/cols.template.pug
@@ -0,0 +1,29 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.list-editable-cols__header(
+    ng-class='::[$ctrl.rowClass, {"list-editable-cols__header__multiple-cols": $ctrl.colDefs.length > 1}]'
+    ng-disabled='$ctrl.ngDisabled'
+)
+    .list-editable-cols__header-cell(ng-repeat='col in ::$ctrl.colDefs' ng-class='::col.cellClass')
+        span.form-field__label
+            | {{ ::col.name }}
+            svg(
+                ng-if='::col.tip'
+                ignite-icon='info'
+                bs-tooltip=''
+                data-title='{{::col.tip}}'
+            )
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-cols/index.js b/modules/frontend/app/components/list-editable/components/list-editable-cols/index.js
new file mode 100644
index 0000000..1e3cfbf
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-cols/index.js
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import cols from './cols.directive';
+import row from './row.directive';
+
+export default angular
+    .module('list-editable-cols', [])
+    .directive('listEditableCols', cols)
+    .directive('listEditableItemView', row)
+    .directive('listEditableItemEdit', row);
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-cols/row.directive.js b/modules/frontend/app/components/list-editable/components/list-editable-cols/row.directive.js
new file mode 100644
index 0000000..8f38331
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-cols/row.directive.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {nonEmpty} from 'app/utils/lodashMixins';
+
+import {ListEditableColsController} from './cols.directive';
+
+/** @returns {ng.IDirective} */
+export default function() {
+    return {
+        require: '?^listEditableCols',
+        /** @param {ListEditableColsController} ctrl */
+        link(scope, el, attr, ctrl) {
+            if (!ctrl || !ctrl.colDefs.length)
+                return;
+
+            const children = el.children();
+
+            if (children.length !== ctrl.colDefs.length)
+                return;
+
+            if (ctrl.rowClass)
+                el.addClass(ctrl.rowClass);
+
+            ctrl.colDefs.forEach(({ cellClass }, index) => {
+                if (nonEmpty(cellClass))
+                    children[index].classList.add(...(Array.isArray(cellClass) ? cellClass : cellClass.split(' ')));
+            });
+        }
+    };
+}
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-one-way/directive.ts b/modules/frontend/app/components/list-editable/components/list-editable-one-way/directive.ts
new file mode 100644
index 0000000..91b0441
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-one-way/directive.ts
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import isMatch from 'lodash/isMatch';
+import {default as ListEditableController, ID} from '../../controller';
+
+export default function listEditableOneWay(): ng.IDirective {
+    return {
+        require: {
+            list: 'listEditable'
+        },
+        bindToController: {
+            onItemChange: '&?',
+            onItemRemove: '&?'
+        },
+        controller: class Controller<T> {
+            list: ListEditableController<T>;
+            onItemChange: ng.ICompiledExpression;
+            onItemRemove: ng.ICompiledExpression;
+
+            $onInit() {
+                this.list.save = (item: T, id: ID) => {
+                    if (!isMatch(this.list.getItem(id), item)) this.onItemChange({$event: item});
+                };
+                this.list.remove = (id: ID) => this.onItemRemove({
+                    $event: this.list.getItem(id)
+                });
+            }
+        }
+    };
+}
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-one-way/index.ts b/modules/frontend/app/components/list-editable/components/list-editable-one-way/index.ts
new file mode 100644
index 0000000..652ac0a
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-one-way/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import directive from './directive';
+
+export default angular
+    .module('ignite-console.list-editable.one-way', [])
+    .directive('listEditableOneWay', directive);
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-save-on-changes/directives.ts b/modules/frontend/app/components/list-editable/components/list-editable-save-on-changes/directives.ts
new file mode 100644
index 0000000..409e907
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-save-on-changes/directives.ts
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {default as ListEditableController, ItemScope} from '../../controller';
+import {ListEditableTransclude} from '../list-editable-transclude/directive';
+
+const CUSTOM_EVENT_TYPE = '$ngModel.change';
+
+/** 
+ * Emits $ngModel.change event on every ngModel.$viewValue change
+ */
+export function ngModel<T>(): ng.IDirective {
+    return {
+        link(scope, el, attr, {ngModel, list}: {ngModel: ng.INgModelController, list?: ListEditableController<T>}) {
+            if (!list)
+                return;
+
+            ngModel.$viewChangeListeners.push(() => {
+                el[0].dispatchEvent(new CustomEvent(CUSTOM_EVENT_TYPE, {bubbles: true, cancelable: true}));
+            });
+        },
+        require: {
+            ngModel: 'ngModel',
+            list: '?^listEditable'
+        }
+    };
+}
+/** 
+ * Triggers $ctrl.save when any ngModel emits $ngModel.change event
+ */
+export function listEditableTransclude<T>(): ng.IDirective {
+    return {
+        link(scope: ItemScope<T>, el, attr, {list, transclude}: {list?: ListEditableController<T>, transclude: ListEditableTransclude<T>}) {
+            if (attr.listEditableTransclude !== 'itemEdit')
+                return;
+
+            if (!list)
+                return;
+
+            let listener = (e) => {
+                e.stopPropagation();
+                scope.$evalAsync(() => {
+                    if (scope.form.$valid) list.save(scope.item, list.id(scope.item, transclude.$index));
+                });
+            };
+
+            el[0].addEventListener(CUSTOM_EVENT_TYPE, listener);
+
+            scope.$on('$destroy', () => {
+                el[0].removeEventListener(CUSTOM_EVENT_TYPE, listener);
+                listener = null;
+            });
+        },
+        require: {
+            list: '?^listEditable',
+            transclude: 'listEditableTransclude'
+        }
+    };
+}
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-save-on-changes/index.ts b/modules/frontend/app/components/list-editable/components/list-editable-save-on-changes/index.ts
new file mode 100644
index 0000000..c73495d
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-save-on-changes/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import {listEditableTransclude, ngModel} from './directives';
+
+export default angular
+    .module('list-editable.save-on-changes', [])
+    .directive('ngModel', ngModel)
+    .directive('listEditableTransclude', listEditableTransclude);
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-transclude/directive.ts b/modules/frontend/app/components/list-editable/components/list-editable-transclude/directive.ts
new file mode 100644
index 0000000..272a423
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-transclude/directive.ts
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {default as ListEditable, ItemScope} from '../../controller';
+
+type TranscludedScope<T> = {$form: ng.IFormController, $item?: T} & ng.IScope
+
+/**
+ * Transcludes list-editable slots and proxies item and form scope values to the slot scope,
+ * also provides a way to retrieve internal list-editable ng-repeat $index by controller getter.
+ * User can provide an alias for $item by setting item-name attribute on transclusion slot element.
+ */
+export class ListEditableTransclude<T> {
+    /**
+     * Transcluded slot name.
+     */
+    slot: string;
+
+    list: ListEditable<T>;
+
+    static $inject = ['$scope', '$element'];
+
+    constructor(private $scope: ItemScope<T>, private $element: JQLite) {}
+
+    $postLink() {
+        this.list.$transclude((clone, transcludedScope: TranscludedScope<T>) => {
+            // Ilya Borisov: at first I tried to use a slave directive to get value from
+            // attribute and set it to ListEditableTransclude controller, but it turns out
+            // this directive would run after list-editable-transclude, so that approach
+            // doesn't work. That's why I decided to access a raw DOM attribute instead.
+            const itemName = clone.attr('item-name') || '$item';
+
+            // We don't want to keep references to any parent scope objects
+            transcludedScope.$on('$destroy', () => {
+                delete transcludedScope[itemName];
+                delete transcludedScope.$form;
+            });
+
+            Object.defineProperties(transcludedScope, {
+                [itemName]: {
+                    get: () => {
+                        // Scope might get destroyed
+                        if (!this.$scope)
+                            return;
+
+                        return this.$scope.item;
+                    },
+                    set: (value) => {
+                        // There are two items: the original one from collection and an item from
+                        // cache that will be saved, so the latter should be the one we set.
+                        if (!this.$scope)
+                            return;
+
+                        this.$scope.item = value;
+                    },
+                    // Allows to delete property later
+                    configurable: true
+                },
+                $form: {
+                    get: () => {
+                        // Scope might get destroyed
+                        if (!this.$scope)
+                            return;
+
+                        return this.$scope.form;
+                    },
+                    // Allows to delete property later
+                    configurable: true
+                }
+            });
+
+            this.$element.append(clone);
+        }, null, this.slot);
+    }
+
+    /**
+     * Returns list-editable ng-repeat $index.
+     */
+    get $index() {
+        if (!this.$scope)
+            return;
+
+        return this.$scope.$index;
+    }
+
+    $onDestroy() {
+        this.$scope = this.$element = null;
+    }
+}
+
+export function listEditableTransclude() {
+    return {
+        restrict: 'A',
+        require: {
+            list: '^listEditable'
+        },
+        scope: false,
+        controller: ListEditableTransclude,
+        bindToController: {
+            slot: '@listEditableTransclude'
+        }
+    };
+}
diff --git a/modules/frontend/app/components/list-editable/components/list-editable-transclude/index.ts b/modules/frontend/app/components/list-editable/components/list-editable-transclude/index.ts
new file mode 100644
index 0000000..6d681a4
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/components/list-editable-transclude/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import {listEditableTransclude} from './directive';
+
+export default angular
+    .module('list-editable.transclude', [])
+    .directive('listEditableTransclude', listEditableTransclude);
diff --git a/modules/frontend/app/components/list-editable/controller.ts b/modules/frontend/app/components/list-editable/controller.ts
new file mode 100644
index 0000000..d4d9da6
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/controller.ts
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+export interface ListEditableNgModel<T> extends ng.INgModelController {
+    $viewValue: T[],
+    editListItem(item: T): void,
+    editListIndex(index: number): void
+}
+
+export type ID = (string | number) & {tag: 'ItemID'}
+
+export type ItemScope<T> = {$index: number, item: T, form: ng.IFormController} & ng.IScope
+
+export default class ListEditable<T extends {_id?: any}> {
+    static $inject = ['$animate', '$element', '$transclude', '$timeout'];
+
+    constructor(
+        $animate: ng.animate.IAnimateService,
+        public $element: JQLite,
+        public $transclude: ng.ITranscludeFunction,
+        private $timeout: ng.ITimeoutService
+    ) {
+        $animate.enabled($element, false);
+        this.hasItemView = $transclude.isSlotFilled('itemView');
+
+        this._cache = new Map();
+    }
+
+    ngModel: ListEditableNgModel<T>;
+    hasItemView: boolean;
+    private _cache: Map<ID, T>;
+
+    id(item: T | undefined, index: number): ID {
+        if (item && item._id)
+            return item._id as ID;
+
+        return index as ID;
+    }
+
+    $onDestroy() {
+        this.$element = null;
+    }
+
+    $onInit() {
+        this.ngModel.$isEmpty = (value) => {
+            return !Array.isArray(value) || !value.length;
+        };
+
+        this.ngModel.editListItem = (item) => {
+            this.$timeout(() => {
+                this.startEditView(this.id(item, this.ngModel.$viewValue.indexOf(item)));
+                // For some reason required validator does not re-run after adding an item,
+                // the $validate call fixes the issue.
+                this.ngModel.$validate();
+            });
+        };
+
+        this.ngModel.editListIndex = (index) => {
+            this.$timeout(() => {
+                this.startEditView(this.id(this.ngModel.$viewValue[index], index));
+                // For some reason required validator does not re-run after adding an item,
+                // the $validate call fixes the issue.
+                this.ngModel.$validate();
+            });
+        };
+    }
+
+    save(item: T, id: ID) {
+        this.ngModel.$setViewValue(
+            this.ngModel.$viewValue.map((v, i) => this.id(v, i) === id ? _.cloneDeep(item) : v)
+        );
+    }
+
+    remove(id: ID): void {
+        this.ngModel.$setViewValue(this.ngModel.$viewValue.filter((v, i) => this.id(v, i) !== id));
+    }
+
+    isEditView(id: ID): boolean {
+        return this._cache.has(id);
+    }
+
+    getEditView(id: ID): T {
+        return this._cache.get(id);
+    }
+
+    getItem(id: ID): T {
+        return this.ngModel.$viewValue.find((v, i) => this.id(v, i) === id);
+    }
+
+    startEditView(id: ID) {
+        this._cache.set(
+            id,
+            _.cloneDeep(this.getItem(id))
+        );
+    }
+
+    stopEditView(data: T, id: ID, form: ng.IFormController) {
+        // By default list-editable saves only valid values, but if you specify {allowInvalid: true}
+        // ng-model-option, then it will always save. Be careful and pay extra attention to validation
+        // when doing so, it's an easy way to miss invalid values this way.
+
+        // Don't close if form is invalid and allowInvalid is turned off (which is default value)
+        if (!form.$valid && !this.ngModel.$options.getOption('allowInvalid'))
+            return;
+
+        this._cache.delete(id);
+
+        this.save(data, id);
+    }
+}
diff --git a/modules/frontend/app/components/list-editable/index.ts b/modules/frontend/app/components/list-editable/index.ts
new file mode 100644
index 0000000..78493e6
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/index.ts
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import component from './component';
+import listEditableCols from './components/list-editable-cols';
+import transclude from './components/list-editable-transclude';
+import listEditableOneWay from './components/list-editable-one-way';
+import addItemButton from './components/list-editable-add-item-button';
+import saveOnChanges from './components/list-editable-save-on-changes';
+
+export default angular
+    .module('ignite-console.list-editable', [
+        addItemButton.name,
+        listEditableCols.name,
+        listEditableOneWay.name,
+        transclude.name,
+        saveOnChanges.name
+    ])
+    .component('listEditable', component);
diff --git a/modules/frontend/app/components/list-editable/style.scss b/modules/frontend/app/components/list-editable/style.scss
new file mode 100644
index 0000000..0db6f7a
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/style.scss
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+list-editable {
+    $min-height: 47px;
+    $index-column-width: 46px;
+    $index-color: #757575;
+
+    display: block;
+    flex: 1;
+    transition: 0.2s opacity;    
+
+    &[disabled] {
+        opacity: 0.5;
+        cursor: not-allowed;
+        pointer-events: none;
+    }
+
+    [list-editable-transclude='itemView'] {
+        flex: 1;
+    }
+
+    &-item-view,
+    &-item-edit,
+    &-no-items {
+        flex: 1;
+        display: block;
+    }
+
+    &-no-items {
+        padding: 8px 20px;
+        display: flex;
+        align-items: center;
+        min-height: $min-height;
+        padding: 8px 20px;
+        margin: -6px 0;
+
+        font-style: italic;
+    }
+
+    .le-body {
+        box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2);
+    }
+
+    .le-row-sort {
+        display: none;
+    }
+
+    .le-row {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        min-height: $min-height;
+        padding: 5px 0;
+        background-color: var(--le-row-bg-color); // Ilya Borisov: does not work in IE11
+        border-top: 1px solid #ddd;
+
+        &:nth-child(odd) {
+            --le-row-bg-color: #ffffff;
+        }
+
+        &:nth-child(even) {
+            --le-row-bg-color: #f9f9f9;
+        }
+
+        &-index,
+        &-cross {
+            display: flex;
+            height: 36px;
+        }
+
+        &-index {
+            width: $index-column-width;
+            flex-basis: $index-column-width;
+            padding-left: 10px;
+            flex-shrink: 0;
+            flex-grow: 0;
+            align-items: center;
+            justify-content: center;
+            color: $index-color;
+        }
+
+        &-sort {
+            display: none;
+        }
+
+        &-cross {
+            [ignite-icon] {
+                width: 12px;
+                height: 12px;
+            }
+        }
+
+        &-item {
+            width: 100%;
+
+            &-view {
+                display: flex;
+                min-height: 36px;
+                align-items: center;
+            }
+        }
+
+        &--editable {
+            position: relative;
+            z-index: 1;
+
+            align-items: flex-start;
+        }
+
+        &--has-item-view {
+            cursor: pointer;
+        }
+
+        &:not(.le-row--has-item-view) {
+            align-items: flex-start;
+        }
+    }
+
+    [divider]:after {
+        content: attr(divider);
+
+        display: inline-flex;
+        justify-content: center;
+        align-self: flex-start;
+
+        width: 20px;
+        height: 36px;
+
+        margin-top: 18px;
+        margin-right: -20px;
+        
+        line-height: 36px;
+    }
+}
diff --git a/modules/frontend/app/components/list-editable/template.pug b/modules/frontend/app/components/list-editable/template.pug
new file mode 100644
index 0000000..19a9507
--- /dev/null
+++ b/modules/frontend/app/components/list-editable/template.pug
@@ -0,0 +1,50 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.le-body
+    .le-row(
+        ng-repeat='item in $ctrl.ngModel.$viewValue track by $ctrl.id(item, $index)'
+        ng-class=`{
+            'le-row--editable': $ctrl.isEditView($ctrl.id(item, $index)),
+            'le-row--has-item-view': $ctrl.hasItemView
+        }`)
+
+        .le-row-sort
+            button.btn-ignite.btn-ignite--link-dashed-secondary
+                svg(ignite-icon='sort')
+
+        .le-row-index
+            span {{ $index+1 }}
+
+        .le-row-item
+            .le-row-item-view(ng-if='$ctrl.hasItemView && !$ctrl.isEditView($ctrl.id(item, $index))' ng-click='$ctrl.startEditView($ctrl.id(item, $index))')
+                div(list-editable-transclude='itemView')
+            div(
+                ng-if='!$ctrl.hasItemView || $ctrl.isEditView($ctrl.id(item, $index))'
+                ignite-on-focus-out='$ctrl.stopEditView(item, $ctrl.id(item, $index), form)'
+                ignite-on-focus-out-ignored-classes='bssm-click-overlay bssm-item-text bssm-item-button'
+            )
+                .le-row-item-view(ng-show='$ctrl.hasItemView' ng-init='$ctrl.startEditView($ctrl.id(item, $index));item = $ctrl.getEditView($ctrl.id(item, $index))')
+                    div(list-editable-transclude='itemView')
+                .le-row-item-edit(ng-form name='form')
+                    div(list-editable-transclude='itemEdit')
+
+        .le-row-cross
+            button.btn-ignite.btn-ignite--link-dashed-secondary(type='button' ng-click='$ctrl.remove($ctrl.id(item, $index))')
+                svg(ignite-icon='cross')
+
+    .le-row(ng-hide='$ctrl.ngModel.$viewValue.length')
+        .le-row-item(ng-transclude='noItems')
diff --git a/modules/frontend/app/components/list-of-registered-users/categories.js b/modules/frontend/app/components/list-of-registered-users/categories.js
new file mode 100644
index 0000000..aa70863
--- /dev/null
+++ b/modules/frontend/app/components/list-of-registered-users/categories.js
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default [
+    {name: 'Actions', visible: false, enableHiding: false},
+    {name: 'User', visible: true, enableHiding: false},
+    {name: 'Email', visible: true, enableHiding: true},
+    {name: 'Activated', visible: false, enableHiding: true},
+    {name: 'Company', visible: true, enableHiding: true},
+    {name: 'Country', visible: true, enableHiding: true},
+    {name: 'Last login', visible: false, enableHiding: true},
+    {name: 'Last activity', visible: true, enableHiding: true},
+    {name: 'Configurations', visible: false, enableHiding: true},
+    {name: 'Total activities', visible: true, enableHiding: true},
+    {name: 'Configuration\'s activities', visible: false, enableHiding: true},
+    {name: 'Queries\' activities', visible: false, enableHiding: true}
+];
diff --git a/modules/frontend/app/components/list-of-registered-users/column-defs.js b/modules/frontend/app/components/list-of-registered-users/column-defs.js
new file mode 100644
index 0000000..b1c71f7
--- /dev/null
+++ b/modules/frontend/app/components/list-of-registered-users/column-defs.js
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const ICON_SORT = '<span ui-grid-one-bind-id-grid="col.uid + \'-sortdir-text\'" ui-grid-visible="col.sort.direction" aria-label="Sort Descending"><i ng-class="{ \'ui-grid-icon-up-dir\': col.sort.direction == asc, \'ui-grid-icon-down-dir\': col.sort.direction == desc, \'ui-grid-icon-blank\': !col.sort.direction }" title="" aria-hidden="true"></i></span>';
+
+const USER_TEMPLATE = '<div class="ui-grid-cell-contents user-cell">' +
+    '<i class="pull-left" ng-class="row.entity.admin ? \'icon-admin\' : \'icon-user\'"></i>&nbsp;<label bs-tooltip data-title="{{ COL_FIELD }}">{{ COL_FIELD }}</label></div>';
+
+const CLUSTER_HEADER_TEMPLATE = `<div class='ui-grid-cell-contents' bs-tooltip data-title='{{ col.headerTooltip(col) }}' data-placement='top'><i class='icon-cluster'></i>${ICON_SORT}</div>`;
+const MODEL_HEADER_TEMPLATE = `<div class='ui-grid-cell-contents' bs-tooltip data-title='{{ col.headerTooltip(col) }}' data-placement='top'><i class='fa fa-object-group'></i>${ICON_SORT}</div>`;
+const CACHE_HEADER_TEMPLATE = `<div class='ui-grid-cell-contents' bs-tooltip data-title='{{ col.headerTooltip(col) }}' data-placement='top'><i class='fa fa-database'></i>${ICON_SORT}</div>`;
+const IGFS_HEADER_TEMPLATE = `<div class='ui-grid-cell-contents' bs-tooltip data-title='{{ col.headerTooltip(col) }}' data-placement='top'><i class='fa fa-folder-o'></i>${ICON_SORT}</div>`;
+
+const EMAIL_TEMPLATE = '<div class="ui-grid-cell-contents"><a bs-tooltip data-title="{{ COL_FIELD }}" ng-href="mailto:{{ COL_FIELD }}">{{ COL_FIELD }}</a></div>';
+const DATE_WITH_TITLE = '<div class="ui-grid-cell-contents"><label bs-tooltip data-title="{{ COL_FIELD | date:\'M/d/yy HH:mm\' }}">{{ COL_FIELD | date:"M/d/yy HH:mm" }}</label></div>';
+const VALUE_WITH_TITLE = '<div class="ui-grid-cell-contents"><label bs-tooltip data-title="{{ COL_FIELD }}">{{ COL_FIELD }}</label></div>';
+
+export default [
+    {name: 'user', enableHiding: false, displayName: 'User', categoryDisplayName: 'User', field: 'userName', cellTemplate: USER_TEMPLATE, minWidth: 160, enableFiltering: true, pinnedLeft: true, filter: { placeholder: 'Filter by name...' }},
+    {name: 'email', displayName: 'Email', categoryDisplayName: 'Email', field: 'email', cellTemplate: EMAIL_TEMPLATE, minWidth: 160, width: 220, enableFiltering: true, filter: { placeholder: 'Filter by email...' }},
+    {name: 'activated', displayName: 'Activated', categoryDisplayName: 'Activated', field: 'activated', width: 220, enableFiltering: true, filter: { placeholder: 'Filter by activation...' }, visible: false},
+    {name: 'company', displayName: 'Company', categoryDisplayName: 'Company', field: 'company', cellTemplate: VALUE_WITH_TITLE, minWidth: 180, enableFiltering: true, filter: { placeholder: 'Filter by company...' }},
+    {name: 'country', displayName: 'Country', categoryDisplayName: 'Country', field: 'countryCode', cellTemplate: VALUE_WITH_TITLE, minWidth: 160, enableFiltering: true, filter: { placeholder: 'Filter by country...' }},
+    {name: 'lastlogin', displayName: 'Last login', categoryDisplayName: 'Last login', field: 'lastLogin', cellTemplate: DATE_WITH_TITLE, minWidth: 135, width: 135, enableFiltering: false, visible: false},
+    {name: 'lastactivity', displayName: 'Last activity', categoryDisplayName: 'Last activity', field: 'lastActivity', cellTemplate: DATE_WITH_TITLE, minWidth: 135, width: 145, enableFiltering: false, visible: true, sort: { direction: 'desc', priority: 0 }},
+    // Configurations
+    {name: 'cfg_clusters', displayName: 'Clusters count', categoryDisplayName: 'Configurations', headerCellTemplate: CLUSTER_HEADER_TEMPLATE, field: 'counters.clusters', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Clusters count', minWidth: 65, width: 65, enableFiltering: false, visible: false},
+    {name: 'cfg_models', displayName: 'Models count', categoryDisplayName: 'Configurations', headerCellTemplate: MODEL_HEADER_TEMPLATE, field: 'counters.models', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Models count', minWidth: 65, width: 65, enableFiltering: false, visible: false},
+    {name: 'cfg_caches', displayName: 'Caches count', categoryDisplayName: 'Configurations', headerCellTemplate: CACHE_HEADER_TEMPLATE, field: 'counters.caches', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Caches count', minWidth: 65, width: 65, enableFiltering: false, visible: false},
+    {name: 'cfg_igfs', displayName: 'IGFS count', categoryDisplayName: 'Configurations', headerCellTemplate: IGFS_HEADER_TEMPLATE, field: 'counters.igfs', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'IGFS count', minWidth: 65, width: 65, enableFiltering: false, visible: false},
+    // Activities Total
+    {name: 'cfg', displayName: 'Cfg', categoryDisplayName: 'Total activities', field: 'activitiesTotal["configuration"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Total count of configuration usages', minWidth: 70, width: 70, enableFiltering: false},
+    {name: 'qry', displayName: 'Qry', categoryDisplayName: 'Total activities', field: 'activitiesTotal["queries"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Total count of queries usages', minWidth: 70, width: 70, enableFiltering: false},
+    {name: 'demo', displayName: 'Demo', categoryDisplayName: 'Total activities', field: 'activitiesTotal["demo"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Total count of demo startup', minWidth: 85, width: 85, enableFiltering: false},
+    {name: 'dnld', displayName: 'Dnld', categoryDisplayName: 'Total activities', field: 'activitiesDetail["/agent/download"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Total count of agent downloads', minWidth: 80, width: 80, enableFiltering: false},
+    {name: 'starts', displayName: 'Starts', categoryDisplayName: 'Total activities', field: 'activitiesDetail["/agent/start"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Total count of agent startup', minWidth: 87, width: 87, enableFiltering: false},
+    // Activities Configuration
+    {name: 'clusters', displayName: 'Clusters', categoryDisplayName: 'Configuration\'s activities', field: 'activitiesDetail["base.configuration.overview"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Configuration clusters', minWidth: 100, width: 100, enableFiltering: false, visible: false},
+    {name: 'clusterBasic', displayName: 'Basic', categoryDisplayName: 'Configuration\'s activities', field: 'activitiesDetail["base.configuration.edit.basic"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Configuration clusters', minWidth: 100, width: 100, enableFiltering: false, visible: false},
+    {name: 'clusterBasicNew', displayName: 'Basic create', categoryDisplayName: 'Configuration\'s activities', field: 'activitiesDetail["/configuration/new/basic"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Configuration clusters', minWidth: 100, width: 150, enableFiltering: false, visible: false},
+    {name: 'clusterAdvancedNew', displayName: 'Adv. Cluster create', categoryDisplayName: 'Configuration\'s activities', field: 'activitiesDetail["/configuration/new/advanced/cluster"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Configuration clusters', minWidth: 100, width: 170, enableFiltering: false, visible: false},
+    {name: 'clusterAdvancedCluster', displayName: 'Adv. Cluster edit', categoryDisplayName: 'Configuration\'s activities', field: 'activitiesDetail["base.configuration.edit.advanced.cluster"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Configuration clusters', minWidth: 100, width: 150, enableFiltering: false, visible: false},
+    {name: 'clusterAdvancedCaches', displayName: 'Adv. Caches', categoryDisplayName: 'Configuration\'s activities', field: 'activitiesDetail["base.configuration.edit.advanced.caches"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Configuration clusters', minWidth: 100, width: 150, enableFiltering: false, visible: false},
+    {name: 'clusterAdvancedCache', displayName: 'Adv. Cache edit', categoryDisplayName: 'Configuration\'s activities', field: 'activitiesDetail["base.configuration.edit.advanced.caches.cache"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Configuration clusters', minWidth: 100, width: 150, enableFiltering: false, visible: false},
+    {name: 'clusterAdvancedModels', displayName: 'Adv. Models', categoryDisplayName: 'Configuration\'s activities', field: 'activitiesDetail["base.configuration.edit.advanced.models"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Configuration clusters', minWidth: 100, width: 150, enableFiltering: false, visible: false},
+    {name: 'clusterAdvancedModel', displayName: 'Adv. Model edit', categoryDisplayName: 'Configuration\'s activities', field: 'activitiesDetail["base.configuration.edit.advanced.models.model"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Configuration clusters', minWidth: 100, width: 150, enableFiltering: false, visible: false},
+    {name: 'clusterAdvancedIGFSs', displayName: 'Adv. IGFSs', categoryDisplayName: 'Configuration\'s activities', field: 'activitiesDetail["base.configuration.edit.advanced.igfs"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Configuration clusters', minWidth: 100, width: 150, enableFiltering: false, visible: false},
+    {name: 'clusterAdvancedIGFS', displayName: 'Adv. IGFS edit', categoryDisplayName: 'Configuration\'s activities', field: 'activitiesDetail["base.configuration.edit.advanced.igfs.igfs"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Configuration clusters', minWidth: 100, width: 150, enableFiltering: false, visible: false},
+    // Activities Queries
+    {name: 'execute', displayName: 'Execute', categoryDisplayName: 'Queries\' activities', field: 'activitiesDetail["/queries/execute"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Query executions', minWidth: 98, width: 98, enableFiltering: false, visible: false},
+    {name: 'explain', displayName: 'Explain', categoryDisplayName: 'Queries\' activities', field: 'activitiesDetail["/queries/explain"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Query explain executions', minWidth: 95, width: 95, enableFiltering: false, visible: false},
+    {name: 'scan', displayName: 'Scan', categoryDisplayName: 'Queries\' activities', field: 'activitiesDetail["/queries/scan"] || 0', cellTemplate: VALUE_WITH_TITLE, type: 'number', cellClass: 'ui-grid-number-cell', headerTooltip: 'Scan query executions', minWidth: 80, width: 80, enableFiltering: false, visible: false}
+];
diff --git a/modules/frontend/app/components/list-of-registered-users/controller.js b/modules/frontend/app/components/list-of-registered-users/controller.js
new file mode 100644
index 0000000..6a7e098
--- /dev/null
+++ b/modules/frontend/app/components/list-of-registered-users/controller.js
@@ -0,0 +1,406 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+import columnDefs from './column-defs';
+import categories from './categories';
+
+import headerTemplate from 'app/primitives/ui-grid-header/index.tpl.pug';
+
+const rowTemplate = `<div
+  ng-repeat="(colRenderIndex, col) in colContainer.renderedColumns track by col.uid"
+  ui-grid-one-bind-id-grid="rowRenderIndex + '-' + col.uid + '-cell'"
+  class="ui-grid-cell"
+  ng-class="{ 'ui-grid-row-header-cell': col.isRowHeader }"
+  role="{{col.isRowHeader ? 'rowheader' : 'gridcell'}}"
+  ui-grid-cell/>`;
+
+const treeAggregationFinalizerFn = function(agg) {
+    return agg.rendered = agg.value;
+};
+
+export default class IgniteListOfRegisteredUsersCtrl {
+    static $inject = ['$scope', '$state', '$filter', 'User', 'uiGridGroupingConstants', 'uiGridPinningConstants', 'IgniteAdminData', 'IgniteNotebookData', 'IgniteConfirm', 'IgniteActivitiesUserDialog'];
+
+    constructor($scope, $state, $filter, User, uiGridGroupingConstants, uiGridPinningConstants, AdminData, NotebookData, Confirm, ActivitiesUserDialog) {
+        this.$state = $state;
+        this.AdminData = AdminData;
+        this.ActivitiesDialogFactory = ActivitiesUserDialog;
+        this.Confirm = Confirm;
+        this.User = User;
+        this.NotebookData = NotebookData;
+
+        const dtFilter = $filter('date');
+
+        this.groupBy = 'user';
+
+        this.selected = [];
+
+        this.params = {
+            startDate: new Date(),
+            endDate: new Date()
+        };
+
+        this.uiGridPinningConstants = uiGridPinningConstants;
+        this.uiGridGroupingConstants = uiGridGroupingConstants;
+
+        User.read().then((user) => this.user = user);
+
+        const companiesExcludeFilter = (renderableRows) => {
+            if (_.isNil(this.params.companiesExclude))
+                return renderableRows;
+
+            _.forEach(renderableRows, (row) => {
+                row.visible = _.isEmpty(this.params.companiesExclude) ||
+                    row.entity.company.toLowerCase().indexOf(this.params.companiesExclude.toLowerCase()) === -1;
+            });
+
+            return renderableRows;
+        };
+
+        this.actionOptions = [
+            {
+                action: 'Become this user',
+                click: () => this.becomeUser(),
+                available: true
+            },
+            {
+                action: 'Revoke admin',
+                click: () => this.toggleAdmin(),
+                available: true
+            },
+            {
+                action: 'Grant admin',
+                click: () => this.toggleAdmin(),
+                available: false
+            },
+            {
+                action: 'Add user',
+                sref: '.createUser',
+                available: true
+            },
+            {
+                action: 'Remove user',
+                click: () => this.removeUser(),
+                available: true
+            },
+            {
+                action: 'Activity detail',
+                click: () => this.showActivities(),
+                available: true
+            }
+        ];
+
+        this._userGridOptions = {
+            columnDefs,
+            categories
+        };
+
+        this.gridOptions = {
+            data: [],
+
+            columnDefs,
+            categories,
+
+            treeRowHeaderAlwaysVisible: true,
+            headerTemplate,
+            columnVirtualizationThreshold: 30,
+            rowTemplate,
+            rowHeight: 46,
+            selectWithCheckboxOnly: true,
+            suppressRemoveSort: false,
+            enableFiltering: true,
+            enableSelectAll: true,
+            enableRowSelection: true,
+            enableFullRowSelection: true,
+            enableColumnMenus: false,
+            multiSelect: false,
+            modifierKeysToMultiSelect: true,
+            noUnselect: false,
+            fastWatch: true,
+            exporterSuppressColumns: ['actions'],
+            exporterCsvColumnSeparator: ';',
+            rowIdentity: (row) => row._id,
+            getRowIdentity: (row) => row._id,
+            onRegisterApi: (api) => {
+                this.gridApi = api;
+
+                api.selection.on.rowSelectionChanged($scope, this._updateSelected.bind(this));
+                api.selection.on.rowSelectionChangedBatch($scope, this._updateSelected.bind(this));
+
+                api.core.on.filterChanged($scope, this._filteredRows.bind(this));
+                api.core.on.rowsVisibleChanged($scope, this._filteredRows.bind(this));
+
+                api.grid.registerRowsProcessor(companiesExcludeFilter, 50);
+
+                $scope.$watch(() => this.gridApi.grid.getVisibleRows().length, (rows) => this.adjustHeight(rows));
+                $scope.$watch(() => this.params.companiesExclude, () => this.gridApi.grid.refreshRows());
+            }
+        };
+
+        /**
+         * @param {{startDate: number, endDate: number}} params
+         */
+        const reloadUsers = (params) => {
+            AdminData.loadUsers(params)
+                .then((data) => {
+                    this.gridOptions.data = data;
+
+                    this.companies = _.values(_.groupBy(data, 'company'));
+                    this.countries = _.values(_.groupBy(data, 'countryCode'));
+
+                    this._refreshRows();
+                });
+        };
+
+        const filterDates = _.debounce(() => {
+            const sdt = this.params.startDate;
+            const edt = this.params.endDate;
+
+            this.exporterCsvFilename = `web_console_users_${dtFilter(sdt, 'yyyy_MM')}.csv`;
+
+            const startDate = Date.UTC(sdt.getFullYear(), sdt.getMonth(), 1);
+            const endDate = Date.UTC(edt.getFullYear(), edt.getMonth() + 1, 1);
+
+            reloadUsers({ startDate, endDate });
+        }, 250);
+
+        $scope.$on('userCreated', filterDates);
+        $scope.$watch(() => this.params.startDate, filterDates);
+        $scope.$watch(() => this.params.endDate, filterDates);
+    }
+
+    adjustHeight(rows) {
+        // Add header height.
+        const height = Math.min(rows, 11) * 48 + 78;
+
+        this.gridApi.grid.element.css('height', height + 'px');
+
+        this.gridApi.core.handleWindowResize();
+    }
+
+    _filteredRows() {
+        const filtered = _.filter(this.gridApi.grid.rows, ({ visible}) => visible);
+
+        this.filteredRows = _.map(filtered, 'entity');
+    }
+
+    _updateSelected() {
+        const ids = this.gridApi.selection.legacyGetSelectedRows().map(({ _id }) => _id).sort();
+
+        if (!_.isEqual(ids, this.selected))
+            this.selected = ids;
+
+        if (ids.length) {
+            const user = this.gridApi.selection.legacyGetSelectedRows()[0];
+            const other = this.user._id !== user._id;
+
+            this.actionOptions[0].available = other; // Become this user.
+            this.actionOptions[1].available = other && user.admin; // Revoke admin.
+            this.actionOptions[2].available = other && !user.admin; // Grant admin.
+            this.actionOptions[4].available = other; // Remove user.
+            this.actionOptions[5].available = true; // Activity detail.
+        }
+        else {
+            this.actionOptions[0].available = false; // Become this user.
+            this.actionOptions[1].available = false; // Revoke admin.
+            this.actionOptions[2].available = false; // Grant admin.
+            this.actionOptions[4].available = false; // Remove user.
+            this.actionOptions[5].available = false; // Activity detail.
+        }
+    }
+
+    _refreshRows() {
+        if (this.gridApi) {
+            this.gridApi.grid.refreshRows()
+                .then(() => this._updateSelected());
+        }
+    }
+
+    becomeUser() {
+        const user = this.gridApi.selection.legacyGetSelectedRows()[0];
+
+        this.AdminData.becomeUser(user._id)
+            .then(() => this.User.load())
+            .then(() => this.$state.go('default-state'))
+            .then(() => this.NotebookData.load());
+    }
+
+    toggleAdmin() {
+        if (!this.gridApi)
+            return;
+
+        const user = this.gridApi.selection.legacyGetSelectedRows()[0];
+
+        if (user.adminChanging)
+            return;
+
+        user.adminChanging = true;
+
+        this.AdminData.toggleAdmin(user)
+            .finally(() => {
+                this._updateSelected();
+
+                user.adminChanging = false;
+            });
+    }
+
+    removeUser() {
+        const user = this.gridApi.selection.legacyGetSelectedRows()[0];
+
+        this.Confirm.confirm(`Are you sure you want to remove user: "${user.userName}"?`)
+            .then(() => this.AdminData.removeUser(user))
+            .then(() => {
+                const i = _.findIndex(this.gridOptions.data, (u) => u._id === user._id);
+
+                if (i >= 0) {
+                    this.gridOptions.data.splice(i, 1);
+                    this.gridApi.selection.clearSelectedRows();
+                }
+
+                this.adjustHeight(this.gridOptions.data.length);
+
+                return this._refreshRows();
+            });
+    }
+
+    showActivities() {
+        const user = this.gridApi.selection.legacyGetSelectedRows()[0];
+
+        return new this.ActivitiesDialogFactory({ user });
+    }
+
+    groupByUser() {
+        this.groupBy = 'user';
+
+        this.gridApi.grouping.clearGrouping();
+        this.gridApi.selection.clearSelectedRows();
+
+        _.forEach(_.filter(this.gridApi.grid.columns, {name: 'company'}), (col) => {
+            this.gridApi.pinning.pinColumn(col, this.uiGridPinningConstants.container.NONE);
+        });
+
+        _.forEach(_.filter(this.gridApi.grid.columns, {name: 'country'}), (col) => {
+            this.gridApi.pinning.pinColumn(col, this.uiGridPinningConstants.container.NONE);
+        });
+
+        this.gridOptions.categories = categories;
+    }
+
+    groupByCompany() {
+        this.groupBy = 'company';
+
+        this.gridApi.grouping.clearGrouping();
+        this.gridApi.selection.clearSelectedRows();
+
+        _.forEach(this.gridApi.grid.columns, (col) => {
+            col.enableSorting = true;
+
+            if (col.colDef.type !== 'number')
+                return;
+
+            this.gridApi.grouping.aggregateColumn(col.colDef.name, this.uiGridGroupingConstants.aggregation.SUM);
+            col.customTreeAggregationFinalizerFn = treeAggregationFinalizerFn;
+        });
+
+        this.gridApi.grouping.aggregateColumn('user', this.uiGridGroupingConstants.aggregation.COUNT);
+        _.forEach(_.filter(this.gridApi.grid.columns, {name: 'user'}), (col) => {
+            col.customTreeAggregationFinalizerFn = treeAggregationFinalizerFn;
+        });
+
+        this.gridApi.grouping.aggregateColumn('lastactivity', this.uiGridGroupingConstants.aggregation.MAX);
+        _.forEach(_.filter(this.gridApi.grid.columns, {name: 'lastactivity'}), (col) => {
+            col.customTreeAggregationFinalizerFn = treeAggregationFinalizerFn;
+        });
+
+        this.gridApi.grouping.groupColumn('company');
+        _.forEach(_.filter(this.gridApi.grid.columns, {name: 'company'}), (col) => {
+            col.customTreeAggregationFinalizerFn = (agg) => agg.rendered = agg.groupVal;
+        });
+
+        // Pinning left company.
+        _.forEach(_.filter(this.gridApi.grid.columns, {name: 'company'}), (col) => {
+            this.gridApi.pinning.pinColumn(col, this.uiGridPinningConstants.container.LEFT);
+        });
+
+        // Unpinning country.
+        _.forEach(_.filter(this.gridApi.grid.columns, {name: 'country'}), (col) => {
+            this.gridApi.pinning.pinColumn(col, this.uiGridPinningConstants.container.NONE);
+        });
+
+        const _categories = _.cloneDeep(categories);
+        // Cut company category.
+        const company = _categories.splice(3, 1)[0];
+        company.selectable = false;
+
+        // Add company as first column.
+        _categories.unshift(company);
+        this.gridOptions.categories = _categories;
+    }
+
+    groupByCountry() {
+        this.groupBy = 'country';
+
+        this.gridApi.grouping.clearGrouping();
+        this.gridApi.selection.clearSelectedRows();
+
+        _.forEach(this.gridApi.grid.columns, (col) => {
+            col.enableSorting = true;
+
+            if (col.colDef.type !== 'number')
+                return;
+
+            this.gridApi.grouping.aggregateColumn(col.colDef.name, this.uiGridGroupingConstants.aggregation.SUM);
+            col.customTreeAggregationFinalizerFn = treeAggregationFinalizerFn;
+        });
+
+        this.gridApi.grouping.aggregateColumn('user', this.uiGridGroupingConstants.aggregation.COUNT);
+        _.forEach(_.filter(this.gridApi.grid.columns, {name: 'user'}), (col) => {
+            col.customTreeAggregationFinalizerFn = treeAggregationFinalizerFn;
+        });
+
+        this.gridApi.grouping.aggregateColumn('lastactivity', this.uiGridGroupingConstants.aggregation.MAX);
+        _.forEach(_.filter(this.gridApi.grid.columns, {name: 'lastactivity'}), (col) => {
+            col.customTreeAggregationFinalizerFn = treeAggregationFinalizerFn;
+        });
+
+        this.gridApi.grouping.groupColumn('country');
+        _.forEach(_.filter(this.gridApi.grid.columns, {name: 'country'}), (col) => {
+            col.customTreeAggregationFinalizerFn = (agg) => agg.rendered = agg.groupVal;
+        });
+
+        // Pinning left country.
+        _.forEach(_.filter(this.gridApi.grid.columns, {name: 'country'}), (col) => {
+            this.gridApi.pinning.pinColumn(col, this.uiGridPinningConstants.container.LEFT);
+        });
+
+        // Unpinning country.
+        _.forEach(_.filter(this.gridApi.grid.columns, {name: 'company'}), (col) => {
+            this.gridApi.pinning.pinColumn(col, this.uiGridPinningConstants.container.NONE);
+        });
+
+        const _categories = _.cloneDeep(categories);
+        // Cut company category.
+        const country = _categories.splice(4, 1)[0];
+        country.selectable = false;
+
+        // Add company as first column.
+        _categories.unshift(country);
+        this.gridOptions.categories = _categories;
+    }
+}
diff --git a/modules/frontend/app/components/list-of-registered-users/index.js b/modules/frontend/app/components/list-of-registered-users/index.js
new file mode 100644
index 0000000..021a826
--- /dev/null
+++ b/modules/frontend/app/components/list-of-registered-users/index.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './style.scss';
+
+import templateUrl from './template.tpl.pug';
+import controller from './controller';
+
+export default angular
+    .module('ignite-console.list-of-registered-users', [])
+    .component('igniteListOfRegisteredUsers', {
+        controller,
+        templateUrl
+    });
diff --git a/modules/frontend/app/components/list-of-registered-users/style.scss b/modules/frontend/app/components/list-of-registered-users/style.scss
new file mode 100644
index 0000000..359a19d
--- /dev/null
+++ b/modules/frontend/app/components/list-of-registered-users/style.scss
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ignite-list-of-registered-users {
+    display: block;
+
+    .user-cell {
+        display: flex;
+        overflow-wrap: normal;
+        white-space: normal;
+
+        label {
+            overflow: hidden;
+            text-overflow: ellipsis;
+        }
+    }
+
+    .form-field--inline:first-child {
+        margin-right: 20px;
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/components/list-of-registered-users/template.tpl.pug b/modules/frontend/app/components/list-of-registered-users/template.tpl.pug
new file mode 100644
index 0000000..ff1c851
--- /dev/null
+++ b/modules/frontend/app/components/list-of-registered-users/template.tpl.pug
@@ -0,0 +1,85 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+ul.tabs.tabs--blue
+    li(role='presentation' ng-class='{ active: $ctrl.groupBy === "user" }')
+        a(ng-click='$ctrl.groupByUser()')
+            span Users
+            span.badge.badge--blue(ng-hide='$ctrl.groupBy === "user"')
+                | {{ $ctrl.gridOptions.data.length }}
+            span.badge.badge--blue(ng-show='$ctrl.groupBy === "user"')
+                | {{ $ctrl.filteredRows.length }}
+    li(role='presentation' ng-class='{ active: $ctrl.groupBy === "company" }')
+        a(ng-click='$ctrl.groupByCompany()')
+            span Companies
+            span.badge.badge--blue {{ $ctrl.companies.length }}
+    li(role='presentation' ng-class='{ active: $ctrl.groupBy === "country" }')
+        a(ng-click='$ctrl.groupByCountry()')
+            span Countries
+            span.badge.badge--blue {{ $ctrl.countries.length }}
+
+.panel--ignite
+    header.header-with-selector
+        div(ng-if='!$ctrl.selected.length')
+            span(ng-if='$ctrl.groupBy === "user"') List of registered users
+            span(ng-if='$ctrl.groupBy === "company"') List of registered companies
+            span(ng-if='$ctrl.groupBy === "country"') List of registered countries
+            grid-column-selector(grid-api='$ctrl.gridApi')
+
+        div(ng-if='$ctrl.selected.length')
+            grid-item-selected(grid-api='$ctrl.gridApi')
+
+        div
+            .form-field--inline
+                +form-field__text({
+                    label: 'Exclude:',
+                    model: '$ctrl.params.companiesExclude',
+                    name: '"exclude"',
+                    placeholder: 'Exclude by company name...'
+                })
+
+            .form-field--inline
+                +form-field__datepicker({
+                    label: 'Period: from',
+                    model: '$ctrl.params.startDate',
+                    name: '"startDate"',
+                    maxdate: '$ctrl.params.endDate'
+                })
+            .form-field--inline
+                +form-field__datepicker({
+                    label: 'to',
+                    model: '$ctrl.params.endDate',
+                    name: '"endDate"',
+                    mindate: '$ctrl.params.startDate'
+                })
+
+            grid-export(file-name='$ctrl.exporterCsvFilename' grid-api='$ctrl.gridApi')
+
+            +ignite-form-field-bsdropdown({
+                label: 'Actions',
+                model: '$ctrl.action',
+                name: 'action',
+                options: '$ctrl.actionOptions'
+            })
+
+    .ignite-grid-table
+        .grid.ui-grid--ignite.ui-grid-disabled-group-selection(ui-grid='$ctrl.gridOptions' ui-grid-resize-columns ui-grid-selection ui-grid-exporter ui-grid-pinning ui-grid-grouping ui-grid-hovering)
+
+    grid-no-data(grid-api='$ctrl.gridApi')
+        grid-no-data-filtered
+           | Nothing to display. Check your filters.
diff --git a/modules/frontend/app/components/no-data/component.ts b/modules/frontend/app/components/no-data/component.ts
new file mode 100644
index 0000000..67df1ed
--- /dev/null
+++ b/modules/frontend/app/components/no-data/component.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import templateUrl from './template.tpl.pug';
+import controller from './controller';
+
+import './style.scss';
+
+export default {
+    controller,
+    templateUrl,
+    transclude: true,
+    bindings: {
+        resultDataStatus: '<',
+        handleClusterInactive: '<'
+    }
+} as ng.IComponentOptions;
diff --git a/modules/frontend/app/components/no-data/controller.ts b/modules/frontend/app/components/no-data/controller.ts
new file mode 100644
index 0000000..fa2b540
--- /dev/null
+++ b/modules/frontend/app/components/no-data/controller.ts
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+import {of} from 'rxjs';
+import {distinctUntilChanged, switchMap} from 'rxjs/operators';
+
+export default class NoDataCmpCtrl {
+    static $inject = ['AgentManager', 'AgentModal'];
+
+    connectionState$ = this.AgentManager.connectionSbj.pipe(
+        switchMap((sbj) => {
+            if (!_.isNil(sbj.cluster) && sbj.cluster.active === false)
+                return of('CLUSTER_INACTIVE');
+
+            return of(sbj.state);
+        }),
+        distinctUntilChanged()
+    );
+
+    backText = 'Close';
+
+    constructor(private AgentManager, private AgentModal) {}
+
+    openAgentMissingDialog() {
+        this.AgentModal.agentDisconnected(this.backText, '.');
+    }
+
+    openNodeMissingDialog() {
+        this.AgentModal.clusterDisconnected(this.backText, '.');
+    }
+}
diff --git a/modules/frontend/app/components/no-data/index.ts b/modules/frontend/app/components/no-data/index.ts
new file mode 100644
index 0000000..8959ba5
--- /dev/null
+++ b/modules/frontend/app/components/no-data/index.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import noDataCmp from './component';
+import './style.scss';
+
+export default angular
+    .module('ignite-console.no-data', [])
+    .component('noData', noDataCmp);
diff --git a/modules/frontend/app/components/no-data/style.scss b/modules/frontend/app/components/no-data/style.scss
new file mode 100644
index 0000000..4f16bed
--- /dev/null
+++ b/modules/frontend/app/components/no-data/style.scss
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+no-data{
+  .data-loading-wrapper {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .spinner-circle {
+      margin-right: 5px;
+    }
+  }
+}
diff --git a/modules/frontend/app/components/no-data/template.tpl.pug b/modules/frontend/app/components/no-data/template.tpl.pug
new file mode 100644
index 0000000..cc3f96c
--- /dev/null
+++ b/modules/frontend/app/components/no-data/template.tpl.pug
@@ -0,0 +1,35 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+div(ng-switch='($ctrl.connectionState$ | async:this)')
+    div(ng-switch-when='AGENT_DISCONNECTED')
+        | Agent is disconnected. #[a(ng-click='$ctrl.openAgentMissingDialog()') Check] agent is up and running.
+
+    div(ng-switch-when='CLUSTER_DISCONNECTED')
+        | Cluster is not available. #[a(ng-click='$ctrl.openNodeMissingDialog()') Check] cluster is up and running and agent is appropriately #[a(href="https://apacheignite-tools.readme.io/docs/getting-started#section-configuration" target="_blank") configured].
+
+    div(ng-switch-when='CLUSTER_INACTIVE')
+        div(ng-if='$ctrl.handleClusterInactive') Cluster is inactive. Please activate cluster.
+        div(ng-if='!$ctrl.handleClusterInactive')
+            ng-transclude
+
+    div(ng-switch-default)
+        .data-loading-wrapper(ng-if='$ctrl.resultDataStatus === "WAITING"')
+            .spinner-circle
+            div Data is loading...
+
+        div(ng-if='$ctrl.resultDataStatus !== "WAITING"')
+            ng-transclude
diff --git a/modules/frontend/app/components/page-admin/controller.ts b/modules/frontend/app/components/page-admin/controller.ts
new file mode 100644
index 0000000..45a5c0a
--- /dev/null
+++ b/modules/frontend/app/components/page-admin/controller.ts
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import UserNotificationsService from '../user-notifications/service';
+
+export default class PageAdminCtrl {
+    static $inject = ['UserNotifications'];
+
+    constructor(private UserNotifications: UserNotificationsService) {}
+
+    changeUserNotifications() {
+        this.UserNotifications.editor();
+    }
+}
diff --git a/modules/frontend/app/components/page-admin/index.js b/modules/frontend/app/components/page-admin/index.js
new file mode 100644
index 0000000..46822b7
--- /dev/null
+++ b/modules/frontend/app/components/page-admin/index.js
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './style.scss';
+
+import controller from './controller';
+import templateUrl from './template.tpl.pug';
+import {default as ActivitiesData} from 'app/core/activities/Activities.data';
+
+/**
+ * @param {import('@uirouter/angularjs').UIRouter} $uiRouter
+ * @param {ActivitiesData} ActivitiesData
+ */
+function registerActivitiesHook($uiRouter, ActivitiesData) {
+    $uiRouter.transitionService.onSuccess({to: 'base.settings.**'}, (transition) => {
+        ActivitiesData.post({group: 'settings', action: transition.targetState().name()});
+    });
+}
+
+registerActivitiesHook.$inject = ['$uiRouter', 'IgniteActivitiesData'];
+
+export default angular
+    .module('ignite-console.page-admin', [])
+    .component('pageAdmin', {
+        controller,
+        templateUrl
+    })
+    .run(registerActivitiesHook);
diff --git a/modules/frontend/app/components/page-admin/style.scss b/modules/frontend/app/components/page-admin/style.scss
new file mode 100644
index 0000000..b61ae00
--- /dev/null
+++ b/modules/frontend/app/components/page-admin/style.scss
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import "public/stylesheets/variables";
+
+.admin-page {
+  .docs-header {
+    display: flex;
+    flex-direction: row;
+    align-items: baseline;
+
+    margin: 40px 0 20px 0;
+
+    h1 {
+      font-size: 24px;
+      margin: 0 10px 0 0;
+    }
+  }
+
+  .panel-heading {
+    cursor: default;
+
+    i {
+      margin-top: 2px;
+      margin-right: 10px;
+    }
+
+    sub {
+      bottom: 0;
+    }
+  }
+
+  .ui-grid-header-cell input {
+    font-weight: normal;
+  }
+
+  .ui-grid-header-cell input {
+    font-weight: normal;
+  }
+
+  .ui-grid-filter-select {
+    width: calc(100% - 10px);
+  }
+
+  .ui-grid-cell-contents > i {
+    line-height: $line-height-base;
+  }
+}
diff --git a/modules/frontend/app/components/page-admin/template.tpl.pug b/modules/frontend/app/components/page-admin/template.tpl.pug
new file mode 100644
index 0000000..763e5a6
--- /dev/null
+++ b/modules/frontend/app/components/page-admin/template.tpl.pug
@@ -0,0 +1,28 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.admin-page
+    .docs-content
+        .docs-header
+            h1 Admin panel
+
+            button.btn-ignite.btn-ignite--link-dashed-secondary(ng-click='$ctrl.changeUserNotifications()')
+                svg.icon-left(ignite-icon='gear')
+                | Set user notifications
+        .docs-body
+            .row
+                .col-xs-12
+                    ignite-list-of-registered-users
diff --git a/modules/frontend/app/components/page-forgot-password/component.js b/modules/frontend/app/components/page-forgot-password/component.js
new file mode 100644
index 0000000..8c70e73
--- /dev/null
+++ b/modules/frontend/app/components/page-forgot-password/component.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+
+import './style.scss';
+
+/** @type {ng.IComponentOptions} */
+export default {
+    controller,
+    template,
+    bindings: {
+        email: '<?'
+    }
+};
diff --git a/modules/frontend/app/components/page-forgot-password/controller.js b/modules/frontend/app/components/page-forgot-password/controller.js
new file mode 100644
index 0000000..86597af
--- /dev/null
+++ b/modules/frontend/app/components/page-forgot-password/controller.js
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class PageForgotPassword {
+    /** @type {string} Optional email to populate form with */
+    email;
+    /** @type {import('./types').IForgotPasswordFormController} */
+    form;
+    /** @type {import('./types').IForgotPasswordData} */
+    data = {email: null};
+    /** @type {string} */
+    serverError = null;
+    /** @type {JQLite} */
+    el;
+
+    static $inject = ['Auth', 'IgniteMessages', 'IgniteFormUtils', '$element'];
+
+    /**
+     * @param {import('app/modules/user/Auth.service').default} Auth
+     */
+    constructor(Auth, IgniteMessages, IgniteFormUtils, el) {
+        this.Auth = Auth;
+        this.IgniteMessages = IgniteMessages;
+        this.IgniteFormUtils = IgniteFormUtils;
+        this.el = el;
+    }
+    /** @param {import('./types').IForgotPasswordFormController} form */
+    canSubmitForm(form) {
+        return form.$error.server ? true : !form.$invalid;
+    }
+    $postLink() {
+        this.el.addClass('public-page');
+        this.form.email.$validators.server = () => !this.serverError;
+    }
+
+    /** @param {string} error */
+    setServerError(error) {
+        this.serverError = error;
+        this.form.email.$validate();
+    }
+
+    /**
+     * @param {{email: ng.IChangesObject<string>}} changes
+     */
+    $onChanges(changes) {
+        if ('email' in changes) this.data.email = changes.email.currentValue;
+    }
+    remindPassword() {
+        this.IgniteFormUtils.triggerValidation(this.form);
+
+        this.setServerError(null);
+
+        if (!this.canSubmitForm(this.form))
+            return;
+
+        return this.Auth.remindPassword(this.data.email).catch((res) => {
+            this.IgniteMessages.showError(null, res.data);
+            this.setServerError(res.data);
+        });
+    }
+}
diff --git a/modules/frontend/app/components/page-forgot-password/index.js b/modules/frontend/app/components/page-forgot-password/index.js
new file mode 100644
index 0000000..dec8fd9
--- /dev/null
+++ b/modules/frontend/app/components/page-forgot-password/index.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+import {registerState} from './run';
+
+export default angular
+    .module('ignite-console.page-forgot-password', [
+        'ui.router',
+        'ignite-console.user'
+    ])
+    .component('pageForgotPassword', component)
+    .run(registerState);
diff --git a/modules/frontend/app/components/page-forgot-password/run.js b/modules/frontend/app/components/page-forgot-password/run.js
new file mode 100644
index 0000000..d9259e9
--- /dev/null
+++ b/modules/frontend/app/components/page-forgot-password/run.js
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import publicTemplate from '../../../views/public.pug';
+
+/**
+ * @param {import("@uirouter/angularjs").UIRouter} $uiRouter
+ */
+export function registerState($uiRouter) {
+    /** @type {import("app/types").IIgniteNg1StateDeclaration} */
+    const state = {
+        name: 'forgotPassword',
+        url: '/forgot-password',
+        views: {
+            '': {
+                template: publicTemplate
+            },
+            'page@forgotPassword': {
+                component: 'pageForgotPassword'
+            }
+        },
+        unsaved: true,
+        tfMetaTags: {
+            title: 'Forgot Password'
+        },
+        resolve: [
+            {
+                token: 'email',
+                deps: ['$uiRouter'],
+                /**
+                 * @param {import('@uirouter/angularjs').UIRouter} $uiRouter
+                 */
+                resolveFn($uiRouter) {
+                    return $uiRouter.stateService.transition.targetState().params().email;
+                }
+            }
+        ]
+    };
+
+    $uiRouter.stateRegistry.register(state);
+}
+
+registerState.$inject = ['$uiRouter'];
diff --git a/modules/frontend/app/components/page-forgot-password/style.scss b/modules/frontend/app/components/page-forgot-password/style.scss
new file mode 100644
index 0000000..7893c62
--- /dev/null
+++ b/modules/frontend/app/components/page-forgot-password/style.scss
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+page-forgot-password {
+    display: flex;
+    flex-direction: column;
+    flex: 1 0 auto;
+
+    p {
+        margin-bottom: 20px;
+    }
+
+    .form-field {
+        margin: 10px 0;
+    }
+
+    .form-footer {
+        padding: 15px 0;
+        text-align: right;
+        display: flex;
+        align-items: center;
+
+        .btn-ignite {
+            margin-left: auto;
+        }
+    }
+}
diff --git a/modules/frontend/app/components/page-forgot-password/template.pug b/modules/frontend/app/components/page-forgot-password/template.pug
new file mode 100644
index 0000000..309e9b9
--- /dev/null
+++ b/modules/frontend/app/components/page-forgot-password/template.pug
@@ -0,0 +1,40 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+h3.public-page__title Forgot password?
+p Enter the email address for your account & we'll email you a link to reset your password.
+form(name='$ctrl.form' novalidate ng-submit='$ctrl.remindPassword()')
+    +form-field__email({
+        label: 'Email:',
+        model: '$ctrl.data.email',
+        name: '"email"',
+        placeholder: 'Input email',
+        required: true
+    })(
+        ng-model-options='{allowInvalid: true}'
+        autocomplete='email'
+        ignite-auto-focus
+        tabindex='0'
+    )
+        +form-field__error({error: 'server', message: `{{$ctrl.serverError}}`})
+    footer.form-footer
+        a(ui-sref='signin') Back to sign in
+        button.btn-ignite.btn-ignite--primary(
+            tabindex='1'
+            type='submit'
+        ) Send it to me
diff --git a/modules/frontend/app/components/page-forgot-password/types.ts b/modules/frontend/app/components/page-forgot-password/types.ts
new file mode 100644
index 0000000..cbcff81
--- /dev/null
+++ b/modules/frontend/app/components/page-forgot-password/types.ts
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface IForgotPasswordData {
+    email: string
+}
+
+export interface IForgotPasswordFormController extends ng.IFormController {
+    email: ng.INgModelController
+}
diff --git a/modules/frontend/app/components/page-landing/index.js b/modules/frontend/app/components/page-landing/index.js
new file mode 100644
index 0000000..b893279
--- /dev/null
+++ b/modules/frontend/app/components/page-landing/index.js
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import baseTemplate from './public.pug';
+import template from './template.pug';
+import './style.scss';
+
+export default angular
+    .module('ignite-console.landing', [
+        'ui.router',
+        'ignite-console.user'
+    ])
+    .component('pageLanding', {
+        template
+    })
+    .config(['$stateProvider', function($stateProvider) {
+        // set up the states
+        $stateProvider
+        .state('landing', {
+            url: '/',
+            views: {
+                '@': {
+                    template: baseTemplate
+                },
+                'page@landing': {
+                    component: 'pageLanding'
+                }
+            },
+            // template: '<page-landing></page-landing>',
+            redirectTo: (trans) => {
+                return trans.injector().get('User').read()
+                    .then(() => {
+                        try {
+                            const {name, params} = JSON.parse(localStorage.getItem('lastStateChangeSuccess'));
+
+                            const restored = trans.router.stateService.target(name, params);
+
+                            return restored.valid() ? restored : 'default-state';
+                        }
+                        catch (ignored) {
+                            return 'default-state';
+                        }
+                    })
+                    .catch(() => true);
+            },
+            unsaved: true
+        });
+    }]);
diff --git a/modules/frontend/app/components/page-landing/public.pug b/modules/frontend/app/components/page-landing/public.pug
new file mode 100644
index 0000000..d9153f5
--- /dev/null
+++ b/modules/frontend/app/components/page-landing/public.pug
@@ -0,0 +1,21 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+web-console-header(hide-menu-button='true')
+    .web-console-header-content__title Management console for Apache Ignite
+    .page-landing__button-signin.btn-ignite.btn-ignite--primary(ui-sref='signin') Sign In
+.content(ui-view='page')
+web-console-footer.web-console-footer__page-bottom
\ No newline at end of file
diff --git a/modules/frontend/app/components/page-landing/style.scss b/modules/frontend/app/components/page-landing/style.scss
new file mode 100644
index 0000000..d7f8831
--- /dev/null
+++ b/modules/frontend/app/components/page-landing/style.scss
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@mixin custom_btn {
+    font-weight: 500;
+    padding: 10px 25px !important;
+}
+
+#signin_show {
+    @include custom_btn;
+}
+
+.page-landing__button-signin {
+    align-self: center;
+    margin-left: 30px;
+    margin-right: var(--page-side-padding) !important;
+    min-width: 80px;
+    font-weight: 500;
+    padding: 10px 25px !important;
+    flex: 0 0 auto;
+}
+
+page-landing {
+    display: block;
+    margin: 0 calc(var(--page-side-padding) * -1) calc(var(--page-side-padding) * -1);
+
+    .btn-custom {
+        @include custom_btn;
+    }
+
+    section.intro-container-wrapper {
+        padding: 40px 0;
+
+        background-color: #f9f9f9;
+        border-bottom: 1px solid #aaaaaa;
+
+        .intro-content {
+            padding-right: 70px;
+        }
+
+        h1 {
+            margin-top: 45px;
+            font-size: 48px;
+            line-height: 55px;
+            font-weight: 300;
+        }
+
+        h2 {
+            margin-bottom: 20px;
+            font-size: 24px;
+            font-style: italic;
+            font-weight: 300;
+        }
+
+        p {
+            font-size: 16px;
+            font-weight: 300;
+            color: #777777;
+        }
+
+        .btn-custom {
+            margin-top: 20px;
+            padding: 10px 40px !important;
+            border-color: #f9f9f9 !important;
+        }
+    }
+
+    section.features-container-wrapper {
+        padding: 25px 0 60px;
+        background-color: #ffffff;
+
+        .section-title {
+            font-size: 38px;
+            font-weight: 300;
+            color: #444444;
+            margin: 30px 0 60px;
+        }
+
+        .feature {
+            margin: 30px 0;
+
+            h3 {
+                font-size: 24px;
+                font-weight: normal;
+                color: #000000;
+                line-height: 28px;
+                margin-bottom: 10px;
+            }
+
+            p {
+                color: #666666;
+                font-size: 16px;
+            }
+
+            i.fa {
+                font-size: 48px;
+                color: #bbbbbb;
+            }
+        }
+    }
+}
diff --git a/modules/frontend/app/components/page-landing/template.pug b/modules/frontend/app/components/page-landing/template.pug
new file mode 100644
index 0000000..a0ddc86
--- /dev/null
+++ b/modules/frontend/app/components/page-landing/template.pug
@@ -0,0 +1,56 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+section.intro-container-wrapper
+    .container
+        .col-lg-6.col-md-6.col-sm-6.col-xs-12.intro-content
+            h1  Web Console
+            h2 An Interactive Configuration Wizard and Management Tool for Apache™ Ignite®
+            p It provides an interactive configuration wizard which helps you create and download configuration files and code snippets for your Apache Ignite projects. Additionally, the tool allows you to automatically load SQL metadata from any RDBMS, run SQL queries on your in-memory cache, and view execution plans, in-memory schema, and streaming charts.
+            a#signup_show.btn.btn-lg.btn-primary.btn-custom(ui-sref='signup') Sign Up
+        .col-lg-6.col-md-6.col-sm-6.col-xs-12
+            img(src='/images/page-landing-ui-sample.png')
+section.features-container-wrapper
+    .container.features-container
+        .section-title The Web Console allows you to:
+        .row
+            .col-lg-6.col-md-6.col-sm-6.col-xs-12.feature
+                .col-lg-2.col-md-2.col-sm-2.col-xs-2
+                    i.fa.fa-sitemap
+                .col-lg-9.col-md-9.col-sm-9.col-xs-9
+                    h3 Configure Apache Ignite clusters and caches
+                    p The Web Console configuration wizard takes you through a step-by-step process that helps you define all the required configuration parameters. The system then generates a ready-to-use project with all the required config files.
+            .col-lg-6.col-md-6.col-sm-6.col-xs-12.feature
+                .col-lg-2.col-md-2.col-sm-2.col-xs-2
+                    i.fa.fa-search
+                .col-lg-9.col-md-9.col-sm-9.col-xs-9
+                    h3 Run free-form SQL queries on #[br] Apache Ignite caches
+                    p By connecting the Web Console to your Apache Ignite cluster, you can execute SQL queries on your in-memory cache. You can also view the execution plan, in-memory schema, and streaming charts for your cluster.
+        .row
+            .col-lg-6.col-md-6.col-sm-6.col-xs-12.feature
+                .col-lg-2.col-md-2.col-sm-2.col-xs-2
+                    i.fa.fa-database
+                .col-lg-9.col-md-9.col-sm-9.col-xs-9
+                    h3 Import database schemas from #[br] virtually any RDBMS
+                    p To speed the creation of your configuration files, the Web Console allows you to automatically import the database schema from virtually any RDBMS including Oracle, SAP, MySQL, PostgreSQL, and many more.
+            .col-lg-6.col-md-6.col-sm-6.col-xs-12.feature
+                .col-lg-2.col-md-2.col-sm-2.col-xs-2
+                    i.fa.fa-gears
+                .col-lg-9.col-md-9.col-sm-9.col-xs-9
+                    h3 Manage the Web Console users
+                    p The Web Console allows you to have accounts with different roles.
+        .align-center.text-center
+            a.btn.btn-lg.btn-primary.btn-custom(ui-sref='signup') Get Started
diff --git a/modules/frontend/app/components/page-password-changed/controller.ts b/modules/frontend/app/components/page-password-changed/controller.ts
new file mode 100644
index 0000000..9696a09
--- /dev/null
+++ b/modules/frontend/app/components/page-password-changed/controller.ts
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {StateService} from '@uirouter/angularjs';
+
+export default class implements ng.IPostLink {
+    static $inject = ['$state', '$timeout', '$element'];
+
+    constructor($state: StateService, $timeout: ng.ITimeoutService, private el: JQLite) {
+        $timeout(() => {
+            $state.go('signin');
+        }, 10000);
+    }
+
+    $postLink() {
+        this.el.addClass('public-page');
+    }
+}
diff --git a/modules/frontend/app/components/page-password-changed/index.js b/modules/frontend/app/components/page-password-changed/index.js
new file mode 100644
index 0000000..903138f
--- /dev/null
+++ b/modules/frontend/app/components/page-password-changed/index.js
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import template from './template.pug';
+import controller from './controller';
+import publicTemplate from '../../../views/public.pug';
+
+import './style.scss';
+
+export default angular
+    .module('ignite-console.page-password-changed', [
+    ])
+    .component('pagePasswordChanged', {
+        template,
+        controller
+    })
+    .config(['$stateProvider', ($stateProvider) => {
+        $stateProvider.state('password.send', {
+            url: '/changed',
+            views: {
+                '@': {
+                    template: publicTemplate
+                },
+                'page@password.send': {
+                    component: 'pagePasswordChanged'
+                }
+            },
+            tfMetaTags: {
+                title: 'Password send'
+            },
+            unsaved: true
+        });
+    }]);
diff --git a/modules/frontend/app/components/page-password-changed/style.scss b/modules/frontend/app/components/page-password-changed/style.scss
new file mode 100644
index 0000000..e32e9b2
--- /dev/null
+++ b/modules/frontend/app/components/page-password-changed/style.scss
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+page-password-changed {
+    display: flex;
+    flex: 1 0 auto;
+    flex-direction: column;
+    min-height: 100%;
+    justify-content: center;
+    align-items: center;
+    max-width: initial !important;
+
+    h2 {
+        margin-bottom: 30px;
+    }
+
+    p {
+        font-size: 16px;
+        text-align: center;
+    }
+}
diff --git a/modules/frontend/app/components/page-password-changed/template.pug b/modules/frontend/app/components/page-password-changed/template.pug
new file mode 100644
index 0000000..82c2cc1
--- /dev/null
+++ b/modules/frontend/app/components/page-password-changed/template.pug
@@ -0,0 +1,21 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+h2 Ready!
+
+p 
+    | Further instructions for password reset have been sent to your e-mail address.#[br]
+    | You'll be redirected back automatically in a few seconds. If not, please #[a(ui-sref='signin') click here].
diff --git a/modules/frontend/app/components/page-password-reset/controller.js b/modules/frontend/app/components/page-password-reset/controller.js
new file mode 100644
index 0000000..b69e566
--- /dev/null
+++ b/modules/frontend/app/components/page-password-reset/controller.js
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class {
+    static $inject = ['$modal', '$http', '$state', 'IgniteMessages', '$element'];
+    /** @type {JQLite} */
+    el;
+
+    /**
+     * @param {mgcrea.ngStrap.modal.IModalService} $modal
+     * @param {ng.IHttpService} $http
+     * @param {import('@uirouter/angularjs').StateService} $state
+     * @param {ReturnType<typeof import('app/services/Messages.service').default>} Messages
+     */
+    constructor($modal, $http, $state, Messages, el) {
+        this.$http = $http;
+        this.$state = $state;
+        this.Messages = Messages;
+        this.el = el;
+    }
+
+    $postLink() {
+        this.el.addClass('public-page');
+    }
+
+    $onInit() {
+        this.$http.post('/api/v1/password/validate/token', {token: this.$state.params.token})
+            .then(({data}) => this.ui = data);
+    }
+
+    // Try to reset user password for provided token.
+    resetPassword() {
+        this.$http.post('/api/v1/password/reset', {token: this.ui.token, password: this.ui.password})
+            .then(() => {
+                this.$state.go('signin');
+
+                this.Messages.showInfo('Password successfully changed');
+            })
+            .catch(({data, state}) => {
+                if (state === 503)
+                    this.$state.go('signin');
+
+                this.Messages.showError(data);
+            });
+    }
+}
diff --git a/modules/frontend/app/components/page-password-reset/index.js b/modules/frontend/app/components/page-password-reset/index.js
new file mode 100644
index 0000000..c07fa23
--- /dev/null
+++ b/modules/frontend/app/components/page-password-reset/index.js
@@ -0,0 +1,65 @@
+/*
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import _ from 'lodash';
+
+import template from './template.pug';
+import controller from './controller';
+import publicTemplate from '../../../views/public.pug';
+
+import './style.scss';
+
+export default angular
+    .module('ignite-console.page-password-reset', [
+    ])
+    .component('pagePasswordReset', {
+        template,
+        controller
+    })
+    .config(['$stateProvider', ($stateProvider) => {
+        // set up the states
+        $stateProvider
+        .state('password', {
+            url: '/password',
+            abstract: true,
+            template: '<ui-view></ui-view>'
+        })
+        .state('password.reset', {
+            url: '/reset?{token}',
+            views: {
+                '@': {
+                    template: publicTemplate
+                },
+                'page@password.reset': {
+                    component: 'pagePasswordReset'
+                }
+            },
+            redirectTo: (trans) => {
+                if (_.isEmpty(trans.params('to').token))
+                    return 'signin';
+
+                return true;
+            },
+            unsaved: true,
+            tfMetaTags: {
+                title: 'Reset password'
+            }
+        });
+    }]);
diff --git a/modules/frontend/app/components/page-password-reset/style.scss b/modules/frontend/app/components/page-password-reset/style.scss
new file mode 100644
index 0000000..05ef953
--- /dev/null
+++ b/modules/frontend/app/components/page-password-reset/style.scss
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+page-password-reset {
+	display: flex;
+    flex: 1 0 auto;
+    flex-direction: column;
+
+    .form-footer {
+        padding: 15px 0;
+        text-align: right;
+        display: flex;
+        align-items: center;
+
+        .btn-ignite {
+            margin-left: auto;
+        }
+    }
+
+    form {
+        display: grid;
+        grid-gap: 10px;
+    }
+}
diff --git a/modules/frontend/app/components/page-password-reset/template.pug b/modules/frontend/app/components/page-password-reset/template.pug
new file mode 100644
index 0000000..c0df3c9
--- /dev/null
+++ b/modules/frontend/app/components/page-password-reset/template.pug
@@ -0,0 +1,60 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+//- This doesn't seem to do anything 😵
+.main-content(ng-if='error')
+    .text-center
+        p {{::$ctrl.ui.error}}
+h3.public-page__title(ng-if-start='$ctrl.ui.token && !$ctrl.ui.error') Reset Password
+form.page-password-reset__grid(name='$ctrl.form' ng-init='reset_info.token = token' ng-if-end)
+    +form-field__email({
+        label: 'E-mail:',
+        model: '$ctrl.ui.email',
+        disabled: true
+    })
+
+    +form-field__password({
+        label: 'New password:',
+        model: '$ctrl.ui.password',
+        name: '"password"',
+        required: true,
+        placeholder: 'New password'
+    })(
+        ignite-auto-focus
+        ignite-on-enter-focus-move='passwordConfirmInput'
+    )
+    +form-field__password({
+        label: 'Confirm password:',
+        model: 'confirm',
+        name: '"passwordConfirm"',
+        required: true,
+        placeholder: 'Confirm new password'
+    })(
+        ignite-on-enter-focus-move='resetForm.$valid && resetPassword(user_info)'
+        ignite-match='$ctrl.ui.password'
+    )
+
+    footer.form-footer
+        a(ui-sref='default-state') Cancel
+        button.btn-ignite.btn-ignite--primary(
+            ng-disabled='$ctrl.form.$invalid'
+            ng-click='$ctrl.resetPassword()'
+        )
+            svg.icon-left(ignite-icon='checkmark')
+            | Save Changes
+
diff --git a/modules/frontend/app/components/page-profile/component.js b/modules/frontend/app/components/page-profile/component.js
new file mode 100644
index 0000000..9578339
--- /dev/null
+++ b/modules/frontend/app/components/page-profile/component.js
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+
+export default {
+    template,
+    controller
+};
diff --git a/modules/frontend/app/components/page-profile/controller.js b/modules/frontend/app/components/page-profile/controller.js
new file mode 100644
index 0000000..d03a5f5
--- /dev/null
+++ b/modules/frontend/app/components/page-profile/controller.js
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+export default class PageProfileController {
+    static $inject = [
+        '$rootScope', '$scope', '$http', 'IgniteLegacyUtils', 'IgniteMessages', 'IgniteFocus', 'IgniteConfirm', 'IgniteCountries', 'User', 'IgniteFormUtils'
+    ];
+
+    /**
+     * @param {ng.IRootScopeService} $root
+     * @param {ng.IScope} $scope
+     * @param {ng.IHttpService} $http
+     * @param {ReturnType<typeof import('app/services/LegacyUtils.service').default>} LegacyUtils
+     * @param {ReturnType<typeof import('app/services/Messages.service').default>} Messages
+     * @param {ReturnType<typeof import('app/services/Focus.service').default>} Focus
+     * @param {import('app/services/Confirm.service').Confirm} Confirm
+     * @param {ReturnType<typeof import('app/services/Countries.service').default>} Countries
+     * @param {ReturnType<typeof import('app/modules/user/User.service').default>} User
+     * @param {ReturnType<typeof import('app/services/FormUtils.service').default>} FormUtils
+     */
+    constructor($root, $scope, $http, LegacyUtils, Messages, Focus, Confirm, Countries, User, FormUtils) {
+        this.$root = $root;
+        this.$scope = $scope;
+        this.$http = $http;
+        this.LegacyUtils = LegacyUtils;
+        this.Messages = Messages;
+        this.Focus = Focus;
+        this.Confirm = Confirm;
+        this.Countries = Countries;
+        this.User = User;
+        this.FormUtils = FormUtils;
+
+        this.isLoading = false;
+    }
+
+    $onInit() {
+        this.ui = {};
+
+        this.User.read()
+            .then((user) => this.ui.user = _.cloneDeep(user));
+
+        this.ui.countries = this.Countries.getAll();
+    }
+
+    onSecurityTokenPanelClose() {
+        this.ui.user.token = this.$root.user.token;
+    }
+
+    generateToken() {
+        this.Confirm.confirm('Are you sure you want to change security token?<br>If you change the token you will need to restart the agent.')
+            .then(() => this.ui.user.token = this.LegacyUtils.randomString(20));
+    }
+
+    onPasswordPanelClose() {
+        delete this.ui.user.password;
+        delete this.ui.user.confirm;
+    }
+
+    saveUser() {
+        if (this.form.$invalid) {
+            this.FormUtils.triggerValidation(this.form);
+
+            return;
+        }
+
+        this.isLoading = true;
+
+        return this.$http.post('/api/v1/profile/save', this.ui.user)
+            .then(this.User.load)
+            .then(() => {
+                this.ui.expandedPassword = this.ui.expandedToken = false;
+
+                this.Messages.showInfo('Profile saved.');
+
+                this.Focus.move('profile-username');
+
+                this.$root.$broadcast('user', this.ui.user);
+            })
+            .catch((res) => this.Messages.showError('Failed to save profile: ', res))
+            .finally(() => this.isLoading = false);
+    }
+}
diff --git a/modules/frontend/app/components/page-profile/index.js b/modules/frontend/app/components/page-profile/index.js
new file mode 100644
index 0000000..d8b1a53
--- /dev/null
+++ b/modules/frontend/app/components/page-profile/index.js
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+import './style.scss';
+
+export default angular
+    .module('ignite-console.page-profile', [
+        'ignite-console.user'
+    ])
+    .config(['$stateProvider', ($stateProvider) => {
+        // set up the states
+        $stateProvider.state('base.settings.profile', {
+            url: '/profile',
+            component: 'pageProfile',
+            permission: 'profile',
+            tfMetaTags: {
+                title: 'User profile'
+            }
+        });
+    }])
+    .component('pageProfile', component);
diff --git a/modules/frontend/app/components/page-profile/style.scss b/modules/frontend/app/components/page-profile/style.scss
new file mode 100644
index 0000000..30dd943
--- /dev/null
+++ b/modules/frontend/app/components/page-profile/style.scss
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+page-profile {
+    max-width: 800px;
+    display: block;
+
+    panel-collapsible {
+        width: 100%;
+    }
+
+    footer {
+        display: flex;
+        justify-content: flex-end;
+    }
+
+    .btn-ignite + .btn-ignite {
+        margin-left: 10px;
+    }
+}
diff --git a/modules/frontend/app/components/page-profile/template.pug b/modules/frontend/app/components/page-profile/template.pug
new file mode 100644
index 0000000..36b381a
--- /dev/null
+++ b/modules/frontend/app/components/page-profile/template.pug
@@ -0,0 +1,161 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+div
+    global-progress-line(is-loading='$ctrl.isLoading')
+
+    header.header-with-selector
+        div
+            h1 User profile
+
+    -var form = '$ctrl.form'
+    form.theme--ignite(name='$ctrl.form' novalidate)
+        .row
+            .col-50
+                +form-field__text({
+                    label: 'First name:',
+                    model: '$ctrl.ui.user.firstName',
+                    name: '"firstName"',
+                    required: true,
+                    placeholder: 'Input first name'
+                })(
+                ignite-auto-focus
+                    ignite-on-enter-focus-move='lastNameInput'
+                )
+            .col-50
+                +form-field__text({
+                    label: 'Last name:',
+                    model: '$ctrl.ui.user.lastName',
+                    name: '"lastName"',
+                    required: true,
+                    placeholder: 'Input last name'
+                })(
+                    ignite-on-enter-focus-move='emailInput'
+                )
+        .row
+            .col-100
+                +form-field__email({
+                    label: 'Email:',
+                    model: '$ctrl.ui.user.email',
+                    name: '"email"',
+                    required: true,
+                    placeholder: 'Input email'
+                })(
+                    ignite-on-enter-focus-move='phoneInput'
+                )
+        .row
+            .col-50
+                +form-field__phone({
+                    label: 'Phone:',
+                    model: '$ctrl.ui.user.phone',
+                    name: '"phone"',
+                    optional: true,
+                    placeholder: 'Input phone (ex.: +15417543010)'
+                })(
+                    ignite-on-enter-focus-move='companyInput'
+                )
+            .col-50
+                +form-field__dropdown({
+                    label: 'Country/Region:',
+                    model: '$ctrl.ui.user.country',
+                    name: '"country"',
+                    required: true,
+                    placeholder: 'Choose your country/region',
+                    options: '$ctrl.ui.countries'
+                })
+        .row
+            .col-100
+                +form-field__text({
+                    label: 'Company:',
+                    model: '$ctrl.ui.user.company',
+                    name: '"company"',
+                    required: true,
+                    placeholder: 'Input company name'
+                })(
+                    ignite-on-enter-focus-move='countryInput'
+                )
+
+        .row#security-token-section
+            .col-100
+                panel-collapsible(
+                    opened='$ctrl.ui.expandedToken'
+                    on-open='$ctrl.ui.expandedToken = true'
+                    on-close='$ctrl.onSecurityTokenPanelClose()'
+                )
+                    panel-title
+                        | {{ $panel.opened ? 'Cancel security token changing...' : 'Show security token...' }}
+                    panel-content
+                        .row
+                            .col-50
+                                +form-field__text({
+                                    label: 'Security Token:',
+                                    model: '$ctrl.ui.user.token',
+                                    tip: 'The security token is used for authentication of Web agent',
+                                    name: '"securityToken"',
+                                    placeholder: 'No security token. Regenerate please.'
+                                })(
+                                    autocomplete='security-token'
+                                    ng-disabled='::true'
+                                    copy-input-value-button='Copy security token to clipboard'
+                                )
+                            .col-50
+                                a(ng-click='$ctrl.generateToken()') Generate Random Security Token?
+
+        .row
+            .col-100
+                panel-collapsible(
+                    opened='$ctrl.ui.expandedPassword'
+                    on-open='$ctrl.ui.expandedPassword = true'
+                    on-close='$ctrl.onPasswordPanelClose()'
+                )
+                    panel-title
+                        | {{ $panel.opened ? 'Cancel password changing...' : 'Change password...' }}
+                    panel-content(ng-if='$panel.opened')
+                        .row
+                            .col-100
+                                +form-field__password({
+                                    label: 'New password:',
+                                    model: '$ctrl.ui.user.password',
+                                    name: '"password"',
+                                    required: true,
+                                    placeholder: 'New password'
+                                })(
+                                    ignite-auto-focus
+                                    ignite-on-enter-focus-move='passwordConfirmInput'
+                                )
+
+                        .row
+                            .col-100
+                                +form-field__password({
+                                    label: 'Confirm password:',
+                                    model: 'user.confirm',
+                                    name: '"passwordConfirm"',
+                                    required: true,
+                                    placeholder: 'Confirm new password'
+                                })(
+                                    ignite-on-enter-focus-move='passwordConfirmInput'
+                                    ignite-match='$ctrl.ui.user.password'
+                                )
+
+    hr
+
+    footer
+        a.btn-ignite.btn-ignite--link-success(type='button' ui-sref='default-state') Cancel
+        button.btn-ignite.btn-ignite--success(ng-click='$ctrl.saveUser()' ng-disabled='$ctrl.isLoading')
+            svg.icon-left(ignite-icon='checkmark')
+            | Save Changes
diff --git a/modules/frontend/app/components/page-queries/component.js b/modules/frontend/app/components/page-queries/component.js
new file mode 100644
index 0000000..e0c1083
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/component.js
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import templateUrl from './template.tpl.pug';
+
+export default {
+    templateUrl,
+    transclude: {
+        queriesButtons: '?queriesButtons',
+        queriesContent: '?queriesContent',
+        queriesTitle: '?queriesTitle'
+    },
+    controller: class Ctrl {
+        static $inject = ['$element', '$rootScope', '$state', 'IgniteNotebook'];
+
+        /**
+         * @param {JQLite} $element       
+         * @param {ng.IRootScopeService} $rootScope     
+         * @param {import('@uirouter/angularjs').StateService} $state         
+         * @param {import('./notebook.service').default} IgniteNotebook
+         */
+        constructor($element, $rootScope, $state, IgniteNotebook) {
+            this.$element = $element;
+            this.$rootScope = $rootScope;
+            this.$state = $state;
+            this.IgniteNotebook = IgniteNotebook;
+        }
+
+        $onInit() {
+            this.loadNotebooks();
+        }
+
+        async loadNotebooks() {
+            const fetchNotebooksPromise = this.IgniteNotebook.read();
+
+            this.notebooks = await fetchNotebooksPromise || [];
+        }
+
+        $postLink() {
+            this.queriesTitle = this.$element.find('.queries-title');
+            this.queriesButtons = this.$element.find('.queries-buttons');
+            this.queriesContent = this.$element.find('.queries-content');
+        }
+    }
+};
diff --git a/modules/frontend/app/components/page-queries/components/queries-notebook/components/ignite-information/information.directive.js b/modules/frontend/app/components/page-queries/components/queries-notebook/components/ignite-information/information.directive.js
new file mode 100644
index 0000000..d7f3529
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/components/queries-notebook/components/ignite-information/information.directive.js
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './information.scss';
+import template from './information.pug';
+
+export default function() {
+    return {
+        scope: {
+            title: '@'
+        },
+        restrict: 'E',
+        template,
+        replace: true,
+        transclude: true
+    };
+}
diff --git a/modules/frontend/app/components/page-queries/components/queries-notebook/components/ignite-information/information.pug b/modules/frontend/app/components/page-queries/components/queries-notebook/components/ignite-information/information.pug
new file mode 100644
index 0000000..aa4d0e9
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/components/queries-notebook/components/ignite-information/information.pug
@@ -0,0 +1,20 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.block-information
+    svg(ignite-icon='attention' ng-if='title')
+    h3(ng-if='title') {{::title}}
+    div(ng-transclude='')
diff --git a/modules/frontend/app/components/page-queries/components/queries-notebook/components/ignite-information/information.scss b/modules/frontend/app/components/page-queries/components/queries-notebook/components/ignite-information/information.scss
new file mode 100644
index 0000000..12bf087
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/components/queries-notebook/components/ignite-information/information.scss
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+$ignite-block-information: #fcfcfc;
+$ignite-block-information-border: #aab8c6;
+$ignite-block-information-icon: #4a6785;
+
+.block-information {
+    position: relative;
+
+    background: $ignite-block-information;
+
+    border-radius: 5px;
+    border: 1px solid $ignite-block-information-border;
+
+    margin: 20px 0;
+    padding: 10px 10px 0 30px;
+
+    > h3 {
+        margin-bottom: 10px;
+    }
+
+    > [ignite-icon] {
+        cursor: default;
+
+        color: $ignite-block-information-icon;
+
+        position: absolute;
+        top: 12px;
+        left: 10px;
+
+        font-size: 16px;
+
+        vertical-align: text-bottom
+    }
+
+    ul {
+        padding-left: 20px;
+    }
+}
+
diff --git a/modules/frontend/app/components/page-queries/components/queries-notebook/components/query-actions-button/component.ts b/modules/frontend/app/components/page-queries/components/queries-notebook/components/query-actions-button/component.ts
new file mode 100644
index 0000000..099efc2
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/components/queries-notebook/components/query-actions-button/component.ts
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import QueryActionsButton from './controller';
+
+export const component: ng.IComponentOptions = {
+    controller: QueryActionsButton,
+    template,
+    bindings: {
+        actions: '<',
+        item: '<'
+    }
+};
diff --git a/modules/frontend/app/components/page-queries/components/queries-notebook/components/query-actions-button/controller.ts b/modules/frontend/app/components/page-queries/components/queries-notebook/components/query-actions-button/controller.ts
new file mode 100644
index 0000000..e71cf46
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/components/queries-notebook/components/query-actions-button/controller.ts
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type QueryActions < T > = Array<{text: string, click?(item: T): any, available?(item: T): boolean}>;
+
+export default class QueryActionButton<T> {
+    static $inject = ['$element'];
+
+    item: T;
+
+    actions: QueryActions<T>;
+
+    boundActions: QueryActions<undefined> = [];
+
+    constructor(private el: JQLite) {}
+
+    $postLink() {
+        this.el[0].classList.add('btn-ignite-group');
+    }
+
+    $onChanges(changes: {actions: ng.IChangesObject<QueryActionButton<T>['actions']>}) {
+        if ('actions' in changes) {
+            this.boundActions = changes.actions.currentValue.map((a) => {
+                const action = {...a};
+
+                const click = () => a.click(this.item);
+
+                Object.defineProperty(action, 'click', {
+                    get: () => {
+                        return typeof a.available === 'function'
+                            ? a.available(this.item) ? click : void 0
+                            : a.available ? click : void 0;
+                    }
+                });
+                return action;
+            });
+        }
+    }
+}
diff --git a/modules/frontend/app/components/page-queries/components/queries-notebook/components/query-actions-button/template.pug b/modules/frontend/app/components/page-queries/components/queries-notebook/components/query-actions-button/template.pug
new file mode 100644
index 0000000..32359bf
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/components/queries-notebook/components/query-actions-button/template.pug
@@ -0,0 +1,28 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+button.btn-ignite.btn-ignite--primary(
+    ng-click='$ctrl.boundActions[0].click()'
+    type='button'
+    ng-disabled='!$ctrl.boundActions[0].click'
+)
+    | {{ ::$ctrl.boundActions[0].text }}
+button.btn-ignite.btn-ignite--primary(
+    bs-dropdown='$ctrl.boundActions'
+    data-placement='bottom-right'
+    type='button'
+)
+    span.icon.fa.fa-caret-down
diff --git a/modules/frontend/app/components/page-queries/components/queries-notebook/controller.ts b/modules/frontend/app/components/page-queries/components/queries-notebook/controller.ts
new file mode 100644
index 0000000..95df0c6
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/components/queries-notebook/controller.ts
@@ -0,0 +1,2369 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+import {nonEmpty, nonNil} from 'app/utils/lodashMixins';
+import id8 from 'app/utils/id8';
+import {defer, EMPTY, from, merge, of, Subject, timer} from 'rxjs';
+import {
+    catchError,
+    distinctUntilChanged,
+    exhaustMap,
+    expand,
+    filter,
+    finalize,
+    first,
+    ignoreElements,
+    map,
+    pluck,
+    switchMap,
+    take,
+    takeUntil,
+    takeWhile,
+    tap
+} from 'rxjs/operators';
+
+import {CSV} from 'app/services/CSV';
+
+import paragraphRateTemplateUrl from 'views/sql/paragraph-rate.tpl.pug';
+import cacheMetadataTemplateUrl from 'views/sql/cache-metadata.tpl.pug';
+import chartSettingsTemplateUrl from 'views/sql/chart-settings.tpl.pug';
+import messageTemplateUrl from 'views/templates/message.tpl.pug';
+
+import {default as Notebook} from '../../notebook.service';
+import {default as MessagesServiceFactory} from 'app/services/Messages.service';
+import {default as LegacyConfirmServiceFactory} from 'app/services/Confirm.service';
+import {default as InputDialog} from 'app/components/input-dialog/input-dialog.service';
+import {QueryActions} from './components/query-actions-button/controller';
+import {CancellationError} from 'app/errors/CancellationError';
+
+// Time line X axis descriptor.
+const TIME_LINE = {value: -1, type: 'java.sql.Date', label: 'TIME_LINE'};
+
+// Row index X axis descriptor.
+const ROW_IDX = {value: -2, type: 'java.lang.Integer', label: 'ROW_IDX'};
+
+const NON_COLLOCATED_JOINS_SINCE = '1.7.0';
+
+const COLLOCATED_QUERY_SINCE = [['2.3.5', '2.4.0'], ['2.4.6', '2.5.0'], ['2.5.1-p13', '2.6.0'], '2.7.0'];
+
+const ENFORCE_JOIN_SINCE = [['1.7.9', '1.8.0'], ['1.8.4', '1.9.0'], '1.9.1'];
+
+const LAZY_QUERY_SINCE = [['2.1.4-p1', '2.2.0'], '2.2.1'];
+
+const DDL_SINCE = [['2.1.6', '2.2.0'], '2.3.0'];
+
+const _fullColName = (col) => {
+    const res = [];
+
+    if (col.schemaName)
+        res.push(col.schemaName);
+
+    if (col.typeName)
+        res.push(col.typeName);
+
+    res.push(col.fieldName);
+
+    return res.join('.');
+};
+
+let paragraphId = 0;
+
+class Paragraph {
+    name: string;
+    qryType: 'scan' | 'query';
+
+    constructor($animate, $timeout, JavaTypes, errorParser, paragraph) {
+        const self = this;
+
+        self.id = 'paragraph-' + paragraphId++;
+        self.qryType = paragraph.qryType || 'query';
+        self.maxPages = 0;
+        self.filter = '';
+        self.useAsDefaultSchema = false;
+        self.localQueryMode = false;
+        self.csvIsPreparing = false;
+        self.scanningInProgress = false;
+
+        self.cancelQuerySubject = new Subject();
+        self.cancelExportSubject = new Subject();
+
+        _.assign(this, paragraph);
+
+        Object.defineProperty(this, 'gridOptions', {value: {
+            enableGridMenu: false,
+            enableColumnMenus: false,
+            flatEntityAccess: true,
+            fastWatch: true,
+            categories: [],
+            rebuildColumns() {
+                if (_.isNil(this.api))
+                    return;
+
+                this.categories.length = 0;
+
+                this.columnDefs = _.reduce(self.meta, (cols, col, idx) => {
+                    cols.push({
+                        displayName: col.fieldName,
+                        headerTooltip: _fullColName(col),
+                        field: idx.toString(),
+                        minWidth: 50,
+                        cellClass: 'cell-left',
+                        visible: self.columnFilter(col)
+                    });
+
+                    this.categories.push({
+                        name: col.fieldName,
+                        visible: self.columnFilter(col),
+                        enableHiding: true
+                    });
+
+                    return cols;
+                }, []);
+
+                $timeout(() => this.api.core.notifyDataChange('column'));
+            },
+            adjustHeight() {
+                if (_.isNil(this.api))
+                    return;
+
+                this.data = self.rows;
+
+                const height = Math.min(self.rows.length, 15) * 30 + 47;
+
+                // Remove header height.
+                this.api.grid.element.css('height', height + 'px');
+
+                $timeout(() => this.api.core.handleWindowResize());
+            },
+            onRegisterApi(api) {
+                $animate.enabled(api.grid.element, false);
+
+                this.api = api;
+
+                this.rebuildColumns();
+
+                this.adjustHeight();
+            }
+        }});
+
+        Object.defineProperty(this, 'chartHistory', {value: []});
+
+        Object.defineProperty(this, 'error', {value: {
+            root: {},
+            message: ''
+        }});
+
+        this.showLoading = (enable) => {
+            if (this.qryType === 'scan')
+                this.scanningInProgress = enable;
+
+            this.loading = enable;
+        };
+
+        this.setError = (err) => {
+            this.error.root = err;
+            this.error.message = errorParser.extractMessage(err);
+
+            let cause = err;
+
+            while (nonNil(cause)) {
+                if (nonEmpty(cause.className) &&
+                    _.includes(['SQLException', 'JdbcSQLException', 'QueryCancelledException'], JavaTypes.shortClassName(cause.className))) {
+                    this.error.message = errorParser.extractMessage(cause.message || cause.className);
+
+                    break;
+                }
+
+                cause = cause.cause;
+            }
+
+            if (_.isEmpty(this.error.message) && nonEmpty(err.className)) {
+                this.error.message = 'Internal cluster error';
+
+                if (nonEmpty(err.className))
+                    this.error.message += ': ' + err.className;
+            }
+        };
+    }
+
+    resultType() {
+        if (_.isNil(this.queryArgs))
+            return null;
+
+        if (nonEmpty(this.error.message))
+            return 'error';
+
+        if (_.isEmpty(this.rows))
+            return 'empty';
+
+        return this.result === 'table' ? 'table' : 'chart';
+    }
+
+    nonRefresh() {
+        return _.isNil(this.rate) || _.isNil(this.rate.stopTime);
+    }
+
+    table() {
+        return this.result === 'table';
+    }
+
+    chart() {
+        return this.result !== 'table' && this.result !== 'none';
+    }
+
+    nonEmpty() {
+        return this.rows && this.rows.length > 0;
+    }
+
+    queryExecuted() {
+        return nonEmpty(this.meta) || nonEmpty(this.error.message);
+    }
+
+    scanExplain() {
+        return this.queryExecuted() && (this.qryType === 'scan' || this.queryArgs.query.startsWith('EXPLAIN '));
+    }
+
+    timeLineSupported() {
+        return this.result !== 'pie';
+    }
+
+    chartColumnsConfigured() {
+        return nonEmpty(this.chartKeyCols) && nonEmpty(this.chartValCols);
+    }
+
+    chartTimeLineEnabled() {
+        return nonEmpty(this.chartKeyCols) && _.eq(this.chartKeyCols[0], TIME_LINE);
+    }
+
+    executionInProgress(showLocal = false) {
+        return this.loading && (this.localQueryMode === showLocal);
+    }
+
+    checkScanInProgress(showLocal = false) {
+        return this.scanningInProgress && (this.localQueryMode === showLocal);
+    }
+
+    cancelRefresh($interval) {
+        if (this.rate && this.rate.stopTime) {
+            $interval.cancel(this.rate.stopTime);
+
+            delete this.rate.stopTime;
+        }
+    }
+
+    reset($interval) {
+        this.meta = [];
+        this.chartColumns = [];
+        this.chartKeyCols = [];
+        this.chartValCols = [];
+        this.error.root = {};
+        this.error.message = '';
+        this.rows = [];
+        this.duration = 0;
+
+        this.cancelRefresh($interval);
+    }
+
+    toJSON() {
+        return {
+            name: this.name,
+            query: this.query,
+            result: this.result,
+            pageSize: this.pageSize,
+            timeLineSpan: this.timeLineSpan,
+            maxPages: this.maxPages,
+            cacheName: this.cacheName,
+            useAsDefaultSchema: this.useAsDefaultSchema,
+            chartsOptions: this.chartsOptions,
+            rate: this.rate,
+            qryType: this.qryType,
+            nonCollocatedJoins: this.nonCollocatedJoins,
+            enforceJoinOrder: this.enforceJoinOrder,
+            lazy: this.lazy,
+            collocated: this.collocated
+        };
+    }
+}
+
+// Controller for SQL notebook screen.
+export class NotebookCtrl {
+    static $inject = ['IgniteInput', '$rootScope', '$scope', '$http', '$q', '$timeout', '$transitions', '$interval', '$animate', '$location', '$anchorScroll', '$state', '$filter', '$modal', '$popover', '$window', 'IgniteLoading', 'IgniteLegacyUtils', 'IgniteMessages', 'IgniteConfirm', 'AgentManager', 'IgniteChartColors', 'IgniteNotebook', 'IgniteNodes', 'uiGridExporterConstants', 'IgniteVersion', 'IgniteActivitiesData', 'JavaTypes', 'IgniteCopyToClipboard', 'CSV', 'IgniteErrorParser', 'DemoInfo'];
+
+    /**
+     * @param {CSV} CSV
+     */
+    constructor(private IgniteInput: InputDialog, $root, private $scope, $http, $q, $timeout, $transitions, $interval, $animate, $location, $anchorScroll, $state, $filter, $modal, $popover, $window, Loading, LegacyUtils, private Messages: ReturnType<typeof MessagesServiceFactory>, private Confirm: ReturnType<typeof LegacyConfirmServiceFactory>, agentMgr, IgniteChartColors, private Notebook: Notebook, Nodes, uiGridExporterConstants, Version, ActivitiesData, JavaTypes, IgniteCopyToClipboard, CSV, errorParser, DemoInfo) {
+        const $ctrl = this;
+
+        this.CSV = CSV;
+        Object.assign(this, { $root, $scope, $http, $q, $timeout, $transitions, $interval, $animate, $location, $anchorScroll, $state, $filter, $modal, $popover, $window, Loading, LegacyUtils, Messages, Confirm, agentMgr, IgniteChartColors, Notebook, Nodes, uiGridExporterConstants, Version, ActivitiesData, JavaTypes, errorParser, DemoInfo });
+
+        // Define template urls.
+        $ctrl.paragraphRateTemplateUrl = paragraphRateTemplateUrl;
+        $ctrl.cacheMetadataTemplateUrl = cacheMetadataTemplateUrl;
+        $ctrl.chartSettingsTemplateUrl = chartSettingsTemplateUrl;
+        $ctrl.demoStarted = false;
+
+        this.isDemo = $root.IgniteDemoMode;
+
+        const _tryStopRefresh = function(paragraph) {
+            paragraph.cancelRefresh($interval);
+        };
+
+        this._stopTopologyRefresh = () => {
+            if ($scope.notebook && $scope.notebook.paragraphs)
+                $scope.notebook.paragraphs.forEach((paragraph) => _tryStopRefresh(paragraph));
+        };
+
+        $scope.caches = [];
+
+        $scope.pageSizesOptions = [
+            {value: 50, label: '50'},
+            {value: 100, label: '100'},
+            {value: 200, label: '200'},
+            {value: 400, label: '400'},
+            {value: 800, label: '800'},
+            {value: 1000, label: '1000'}
+        ];
+
+        $scope.maxPages = [
+            {label: 'Unlimited', value: 0},
+            {label: '1', value: 1},
+            {label: '5', value: 5},
+            {label: '10', value: 10},
+            {label: '20', value: 20},
+            {label: '50', value: 50},
+            {label: '100', value: 100}
+        ];
+
+        $scope.timeLineSpans = ['1', '5', '10', '15', '30'];
+
+        $scope.aggregateFxs = ['FIRST', 'LAST', 'MIN', 'MAX', 'SUM', 'AVG', 'COUNT'];
+
+        $scope.modes = LegacyUtils.mkOptions(['PARTITIONED', 'REPLICATED', 'LOCAL']);
+
+        $scope.loadingText = $root.IgniteDemoMode ? 'Demo grid is starting. Please wait...' : 'Loading query notebook screen...';
+
+        $scope.timeUnit = [
+            {value: 1000, label: 'seconds', short: 's'},
+            {value: 60000, label: 'minutes', short: 'm'},
+            {value: 3600000, label: 'hours', short: 'h'}
+        ];
+
+        $scope.metadata = [];
+
+        $scope.metaFilter = '';
+
+        $scope.metaOptions = {
+            nodeChildren: 'children',
+            dirSelectable: true,
+            injectClasses: {
+                iExpanded: 'fa fa-minus-square-o',
+                iCollapsed: 'fa fa-plus-square-o'
+            }
+        };
+
+        const maskCacheName = $filter('defaultName');
+
+        // We need max 1800 items to hold history for 30 mins in case of refresh every second.
+        const HISTORY_LENGTH = 1800;
+
+        const MAX_VAL_COLS = IgniteChartColors.length;
+
+        $anchorScroll.yOffset = 55;
+
+        $scope.chartColor = function(index) {
+            return {color: 'white', 'background-color': IgniteChartColors[index]};
+        };
+
+        function _chartNumber(arr, idx, dflt) {
+            if (idx >= 0 && arr && arr.length > idx && _.isNumber(arr[idx]))
+                return arr[idx];
+
+            return dflt;
+        }
+
+        function _min(rows, idx, dflt) {
+            let min = _chartNumber(rows[0], idx, dflt);
+
+            _.forEach(rows, (row) => {
+                const v = _chartNumber(row, idx, dflt);
+
+                if (v < min)
+                    min = v;
+            });
+
+            return min;
+        }
+
+        function _max(rows, idx, dflt) {
+            let max = _chartNumber(rows[0], idx, dflt);
+
+            _.forEach(rows, (row) => {
+                const v = _chartNumber(row, idx, dflt);
+
+                if (v > max)
+                    max = v;
+            });
+
+            return max;
+        }
+
+        function _sum(rows, idx) {
+            let sum = 0;
+
+            _.forEach(rows, (row) => sum += _chartNumber(row, idx, 0));
+
+            return sum;
+        }
+
+        function _aggregate(rows, aggFx, idx, dflt) {
+            const len = rows.length;
+
+            switch (aggFx) {
+                case 'FIRST':
+                    return _chartNumber(rows[0], idx, dflt);
+
+                case 'LAST':
+                    return _chartNumber(rows[len - 1], idx, dflt);
+
+                case 'MIN':
+                    return _min(rows, idx, dflt);
+
+                case 'MAX':
+                    return _max(rows, idx, dflt);
+
+                case 'SUM':
+                    return _sum(rows, idx);
+
+                case 'AVG':
+                    return len > 0 ? _sum(rows, idx) / len : 0;
+
+                case 'COUNT':
+                    return len;
+
+                default:
+            }
+
+            return 0;
+        }
+
+        function _chartLabel(arr, idx, dflt) {
+            if (arr && arr.length > idx && _.isString(arr[idx]))
+                return arr[idx];
+
+            return dflt;
+        }
+
+        function _chartDatum(paragraph) {
+            let datum = [];
+
+            if (paragraph.chartColumnsConfigured()) {
+                paragraph.chartValCols.forEach(function(valCol) {
+                    let index = 0;
+                    let values = [];
+                    const colIdx = valCol.value;
+
+                    if (paragraph.chartTimeLineEnabled()) {
+                        const aggFx = valCol.aggFx;
+                        const colLbl = valCol.label + ' [' + aggFx + ']';
+
+                        if (paragraph.charts && paragraph.charts.length === 1)
+                            datum = paragraph.charts[0].data;
+
+                        const chartData = _.find(datum, {series: valCol.label});
+
+                        const leftBound = new Date();
+                        leftBound.setMinutes(leftBound.getMinutes() - parseInt(paragraph.timeLineSpan, 10));
+
+                        if (chartData) {
+                            const lastItem = _.last(paragraph.chartHistory);
+
+                            values = chartData.values;
+
+                            values.push({
+                                x: lastItem.tm,
+                                y: _aggregate(lastItem.rows, aggFx, colIdx, index++)
+                            });
+
+                            while (values.length > 0 && values[0].x < leftBound)
+                                values.shift();
+                        }
+                        else {
+                            _.forEach(paragraph.chartHistory, (history) => {
+                                if (history.tm >= leftBound) {
+                                    values.push({
+                                        x: history.tm,
+                                        y: _aggregate(history.rows, aggFx, colIdx, index++)
+                                    });
+                                }
+                            });
+
+                            datum.push({series: valCol.label, key: colLbl, values});
+                        }
+                    }
+                    else {
+                        index = paragraph.total;
+
+                        values = _.map(paragraph.rows, function(row) {
+                            const xCol = paragraph.chartKeyCols[0].value;
+
+                            const v = {
+                                x: _chartNumber(row, xCol, index),
+                                xLbl: _chartLabel(row, xCol, null),
+                                y: _chartNumber(row, colIdx, index)
+                            };
+
+                            index++;
+
+                            return v;
+                        });
+
+                        datum.push({series: valCol.label, key: valCol.label, values});
+                    }
+                });
+            }
+
+            return datum;
+        }
+
+        function _xX(d) {
+            return d.x;
+        }
+
+        function _yY(d) {
+            return d.y;
+        }
+
+        function _xAxisTimeFormat(d) {
+            return d3.time.format('%X')(new Date(d));
+        }
+
+        const _intClasses = ['java.lang.Byte', 'java.lang.Integer', 'java.lang.Long', 'java.lang.Short'];
+
+        function _intType(cls) {
+            return _.includes(_intClasses, cls);
+        }
+
+        const _xAxisWithLabelFormat = function(paragraph) {
+            return function(d) {
+                const values = paragraph.charts[0].data[0].values;
+
+                const fmt = _intType(paragraph.chartKeyCols[0].type) ? 'd' : ',.2f';
+
+                const dx = values[d];
+
+                if (!dx)
+                    return d3.format(fmt)(d);
+
+                const lbl = dx.xLbl;
+
+                return lbl ? lbl : d3.format(fmt)(d);
+            };
+        };
+
+        function _xAxisLabel(paragraph) {
+            return _.isEmpty(paragraph.chartKeyCols) ? 'X' : paragraph.chartKeyCols[0].label;
+        }
+
+        const _yAxisFormat = function(d) {
+            const fmt = d < 1000 ? ',.2f' : '.3s';
+
+            return d3.format(fmt)(d);
+        };
+
+        function _updateCharts(paragraph) {
+            $timeout(() => _.forEach(paragraph.charts, (chart) => chart.api.update()), 100);
+        }
+
+        function _updateChartsWithData(paragraph, newDatum) {
+            $timeout(() => {
+                if (!paragraph.chartTimeLineEnabled()) {
+                    const chartDatum = paragraph.charts[0].data;
+
+                    chartDatum.length = 0;
+
+                    _.forEach(newDatum, (series) => chartDatum.push(series));
+                }
+
+                paragraph.charts[0].api.update();
+            });
+        }
+
+        function _yAxisLabel(paragraph) {
+            const cols = paragraph.chartValCols;
+
+            const tml = paragraph.chartTimeLineEnabled();
+
+            return _.isEmpty(cols) ? 'Y' : _.map(cols, function(col) {
+                let lbl = col.label;
+
+                if (tml)
+                    lbl += ' [' + col.aggFx + ']';
+
+                return lbl;
+            }).join(', ');
+        }
+
+        function _barChart(paragraph) {
+            const datum = _chartDatum(paragraph);
+
+            if (_.isEmpty(paragraph.charts)) {
+                const stacked = paragraph.chartsOptions && paragraph.chartsOptions.barChart
+                    ? paragraph.chartsOptions.barChart.stacked
+                    : true;
+
+                const options = {
+                    chart: {
+                        type: 'multiBarChart',
+                        height: 400,
+                        margin: {left: 70},
+                        duration: 0,
+                        x: _xX,
+                        y: _yY,
+                        xAxis: {
+                            axisLabel: _xAxisLabel(paragraph),
+                            tickFormat: paragraph.chartTimeLineEnabled() ? _xAxisTimeFormat : _xAxisWithLabelFormat(paragraph),
+                            showMaxMin: false
+                        },
+                        yAxis: {
+                            axisLabel: _yAxisLabel(paragraph),
+                            tickFormat: _yAxisFormat
+                        },
+                        color: IgniteChartColors,
+                        stacked,
+                        showControls: true,
+                        legend: {
+                            vers: 'furious',
+                            margin: {right: -15}
+                        }
+                    }
+                };
+
+                paragraph.charts = [{options, data: datum}];
+
+                _updateCharts(paragraph);
+            }
+            else
+                _updateChartsWithData(paragraph, datum);
+        }
+
+        function _pieChartDatum(paragraph) {
+            const datum = [];
+
+            if (paragraph.chartColumnsConfigured() && !paragraph.chartTimeLineEnabled()) {
+                paragraph.chartValCols.forEach(function(valCol) {
+                    let index = paragraph.total;
+
+                    const values = _.map(paragraph.rows, (row) => {
+                        const xCol = paragraph.chartKeyCols[0].value;
+
+                        const v = {
+                            x: xCol < 0 ? index : row[xCol],
+                            y: _chartNumber(row, valCol.value, index)
+                        };
+
+                        // Workaround for known problem with zero values on Pie chart.
+                        if (v.y === 0)
+                            v.y = 0.0001;
+
+                        index++;
+
+                        return v;
+                    });
+
+                    datum.push({series: paragraph.chartKeyCols[0].label, key: valCol.label, values});
+                });
+            }
+
+            return datum;
+        }
+
+        function _pieChart(paragraph) {
+            let datum = _pieChartDatum(paragraph);
+
+            if (datum.length === 0)
+                datum = [{values: []}];
+
+            paragraph.charts = _.map(datum, function(data) {
+                return {
+                    options: {
+                        chart: {
+                            type: 'pieChart',
+                            height: 400,
+                            duration: 0,
+                            x: _xX,
+                            y: _yY,
+                            showLabels: true,
+                            labelThreshold: 0.05,
+                            labelType: 'percent',
+                            donut: true,
+                            donutRatio: 0.35,
+                            legend: {
+                                vers: 'furious',
+                                margin: {right: -15}
+                            }
+                        },
+                        title: {
+                            enable: true,
+                            text: data.key
+                        }
+                    },
+                    data: data.values
+                };
+            });
+
+            _updateCharts(paragraph);
+        }
+
+        function _lineChart(paragraph) {
+            const datum = _chartDatum(paragraph);
+
+            if (_.isEmpty(paragraph.charts)) {
+                const options = {
+                    chart: {
+                        type: 'lineChart',
+                        height: 400,
+                        margin: { left: 70 },
+                        duration: 0,
+                        x: _xX,
+                        y: _yY,
+                        xAxis: {
+                            axisLabel: _xAxisLabel(paragraph),
+                            tickFormat: paragraph.chartTimeLineEnabled() ? _xAxisTimeFormat : _xAxisWithLabelFormat(paragraph),
+                            showMaxMin: false
+                        },
+                        yAxis: {
+                            axisLabel: _yAxisLabel(paragraph),
+                            tickFormat: _yAxisFormat
+                        },
+                        color: IgniteChartColors,
+                        useInteractiveGuideline: true,
+                        legend: {
+                            vers: 'furious',
+                            margin: {right: -15}
+                        }
+                    }
+                };
+
+                paragraph.charts = [{options, data: datum}];
+
+                _updateCharts(paragraph);
+            }
+            else
+                _updateChartsWithData(paragraph, datum);
+        }
+
+        function _areaChart(paragraph) {
+            const datum = _chartDatum(paragraph);
+
+            if (_.isEmpty(paragraph.charts)) {
+                const style = paragraph.chartsOptions && paragraph.chartsOptions.areaChart
+                    ? paragraph.chartsOptions.areaChart.style
+                    : 'stack';
+
+                const options = {
+                    chart: {
+                        type: 'stackedAreaChart',
+                        height: 400,
+                        margin: {left: 70},
+                        duration: 0,
+                        x: _xX,
+                        y: _yY,
+                        xAxis: {
+                            axisLabel: _xAxisLabel(paragraph),
+                            tickFormat: paragraph.chartTimeLineEnabled() ? _xAxisTimeFormat : _xAxisWithLabelFormat(paragraph),
+                            showMaxMin: false
+                        },
+                        yAxis: {
+                            axisLabel: _yAxisLabel(paragraph),
+                            tickFormat: _yAxisFormat
+                        },
+                        color: IgniteChartColors,
+                        style,
+                        legend: {
+                            vers: 'furious',
+                            margin: {right: -15}
+                        }
+                    }
+                };
+
+                paragraph.charts = [{options, data: datum}];
+
+                _updateCharts(paragraph);
+            }
+            else
+                _updateChartsWithData(paragraph, datum);
+        }
+
+        function _chartApplySettings(paragraph, resetCharts) {
+            if (resetCharts)
+                paragraph.charts = [];
+
+            if (paragraph.chart() && paragraph.nonEmpty()) {
+                switch (paragraph.result) {
+                    case 'bar':
+                        _barChart(paragraph);
+                        break;
+
+                    case 'pie':
+                        _pieChart(paragraph);
+                        break;
+
+                    case 'line':
+                        _lineChart(paragraph);
+                        break;
+
+                    case 'area':
+                        _areaChart(paragraph);
+                        break;
+
+                    default:
+                }
+            }
+        }
+
+        $scope.chartRemoveKeyColumn = function(paragraph, index) {
+            paragraph.chartKeyCols.splice(index, 1);
+
+            _chartApplySettings(paragraph, true);
+        };
+
+        $scope.chartRemoveValColumn = function(paragraph, index) {
+            paragraph.chartValCols.splice(index, 1);
+
+            _chartApplySettings(paragraph, true);
+        };
+
+        $scope.chartAcceptKeyColumn = function(paragraph, item) {
+            const accepted = _.findIndex(paragraph.chartKeyCols, item) < 0;
+
+            if (accepted) {
+                paragraph.chartKeyCols = [item];
+
+                _chartApplySettings(paragraph, true);
+            }
+
+            return false;
+        };
+
+        const _numberClasses = ['java.math.BigDecimal', 'java.lang.Byte', 'java.lang.Double',
+            'java.lang.Float', 'java.lang.Integer', 'java.lang.Long', 'java.lang.Short'];
+
+        const _numberType = function(cls) {
+            return _.includes(_numberClasses, cls);
+        };
+
+        $scope.chartAcceptValColumn = function(paragraph, item) {
+            const valCols = paragraph.chartValCols;
+
+            const accepted = _.findIndex(valCols, item) < 0 && item.value >= 0 && _numberType(item.type);
+
+            if (accepted) {
+                if (valCols.length === MAX_VAL_COLS - 1)
+                    valCols.shift();
+
+                valCols.push(item);
+
+                _chartApplySettings(paragraph, true);
+            }
+
+            return false;
+        };
+
+        $scope.scrollParagraphs = [];
+
+        $scope.rebuildScrollParagraphs = function() {
+            $scope.scrollParagraphs = $scope.notebook.paragraphs.map(function(paragraph) {
+                return {
+                    text: paragraph.name,
+                    click: 'scrollToParagraph("' + paragraph.id + '")'
+                };
+            });
+        };
+
+        $scope.scrollToParagraph = (id) => {
+            const idx = _.findIndex($scope.notebook.paragraphs, {id});
+
+            if (idx >= 0) {
+                if (!_.includes($scope.notebook.expandedParagraphs, idx))
+                    $scope.notebook.expandedParagraphs = $scope.notebook.expandedParagraphs.concat([idx]);
+
+                if ($scope.notebook.paragraphs[idx].ace)
+                    setTimeout(() => $scope.notebook.paragraphs[idx].ace.focus());
+            }
+
+            $location.hash(id);
+
+            $anchorScroll();
+        };
+
+        const _hideColumn = (col) => col.fieldName !== '_KEY' && col.fieldName !== '_VAL';
+
+        const _allColumn = () => true;
+
+        $scope.aceInit = function(paragraph) {
+            return function(editor) {
+                editor.setAutoScrollEditorIntoView(true);
+                editor.$blockScrolling = Infinity;
+
+                const renderer = editor.renderer;
+
+                renderer.setHighlightGutterLine(false);
+                renderer.setShowPrintMargin(false);
+                renderer.setOption('fontFamily', 'monospace');
+                renderer.setOption('fontSize', '14px');
+                renderer.setOption('minLines', '5');
+                renderer.setOption('maxLines', '15');
+
+                editor.setTheme('ace/theme/chrome');
+
+                Object.defineProperty(paragraph, 'ace', { value: editor });
+            };
+        };
+
+        /**
+         * Update caches list.
+         */
+        const _refreshCaches = () => {
+            return agentMgr.publicCacheNames()
+                .then((cacheNames) => {
+                    $scope.caches = _.sortBy(_.map(cacheNames, (name) => ({
+                        label: maskCacheName(name, true),
+                        value: name
+                    })), (cache) => cache.label.toLowerCase());
+
+                    _.forEach($scope.notebook.paragraphs, (paragraph) => {
+                        if (!_.includes(cacheNames, paragraph.cacheName))
+                            paragraph.cacheName = _.head(cacheNames);
+                    });
+
+                    // Await for demo caches.
+                    if (!$ctrl.demoStarted && $root.IgniteDemoMode && nonEmpty(cacheNames)) {
+                        $ctrl.demoStarted = true;
+
+                        Loading.finish('sqlLoading');
+
+                        _.forEach($scope.notebook.paragraphs, (paragraph) => $scope.execute(paragraph));
+                    }
+
+                    $scope.$applyAsync();
+                })
+                .catch((err) => Messages.showError(err));
+        };
+
+        const _startWatch = () => {
+            const finishLoading$ = defer(() => {
+                if (!$root.IgniteDemoMode)
+                    Loading.finish('sqlLoading');
+            }).pipe(take(1));
+
+            const refreshCaches = (period) => {
+                return merge(timer(0, period).pipe(exhaustMap(() => _refreshCaches())), finishLoading$);
+            };
+
+            const cluster$ = agentMgr.connectionSbj.pipe(
+                pluck('cluster'),
+                distinctUntilChanged(),
+                tap((cluster) => {
+                    this.clusterIsAvailable = (!!cluster && cluster.active === true) || agentMgr.isDemoMode();
+                })
+            );
+
+            this.refresh$ = cluster$.pipe(
+                switchMap((cluster) => {
+                    if (!cluster && !agentMgr.isDemoMode()) {
+                        return of(EMPTY).pipe(
+                            tap(() => {
+                                $scope.caches = [];
+                            })
+                        );
+                    }
+
+                    return of(cluster).pipe(
+                        tap(() => Loading.start('sqlLoading')),
+                        tap(() => {
+                            _.forEach($scope.notebook.paragraphs, (paragraph) => {
+                                paragraph.reset($interval);
+                            });
+                        }),
+                        switchMap(() => refreshCaches(5000))
+                    );
+                })
+            );
+
+            this.subscribers$ = merge(this.refresh$).subscribe();
+        };
+
+        const _newParagraph = (paragraph) => {
+            return new Paragraph($animate, $timeout, JavaTypes, errorParser, paragraph);
+        };
+
+        Notebook.find($state.params.noteId)
+            .then((notebook) => {
+                $scope.notebook = _.cloneDeep(notebook);
+
+                $scope.notebook_name = $scope.notebook.name;
+
+                if (!$scope.notebook.expandedParagraphs)
+                    $scope.notebook.expandedParagraphs = [];
+
+                if (!$scope.notebook.paragraphs)
+                    $scope.notebook.paragraphs = [];
+
+                $scope.notebook.paragraphs = _.map($scope.notebook.paragraphs, (p) => _newParagraph(p));
+
+                if (_.isEmpty($scope.notebook.paragraphs))
+                    $scope.addQuery();
+                else
+                    $scope.rebuildScrollParagraphs();
+            })
+            .then(() => {
+                if ($root.IgniteDemoMode && sessionStorage.showDemoInfo !== 'true') {
+                    sessionStorage.showDemoInfo = 'true';
+
+                    this.DemoInfo.show().then(_startWatch);
+                } else
+                    _startWatch();
+            })
+            .catch(() => {
+                $scope.notebookLoadFailed = true;
+
+                Loading.finish('sqlLoading');
+            });
+
+        $scope.renameNotebook = (name) => {
+            if (!name)
+                return;
+
+            if ($scope.notebook.name !== name) {
+                const prevName = $scope.notebook.name;
+
+                $scope.notebook.name = name;
+
+                Notebook.save($scope.notebook)
+                    .then(() => $scope.notebook.edit = false)
+                    .catch((err) => {
+                        $scope.notebook.name = prevName;
+
+                        Messages.showError(err);
+                    });
+            }
+            else
+                $scope.notebook.edit = false;
+        };
+
+        $scope.removeNotebook = (notebook) => Notebook.remove(notebook);
+
+        $scope.addParagraph = (paragraph, sz) => {
+            if ($scope.caches && $scope.caches.length > 0)
+                paragraph.cacheName = _.head($scope.caches).value;
+
+            $scope.notebook.paragraphs.push(paragraph);
+
+            $scope.notebook.expandedParagraphs.push(sz);
+
+            $scope.rebuildScrollParagraphs();
+
+            $location.hash(paragraph.id);
+        };
+
+        $scope.addQuery = function() {
+            const sz = $scope.notebook.paragraphs.length;
+
+            ActivitiesData.post({ group: 'sql', action: '/queries/add/query' });
+
+            const paragraph = _newParagraph({
+                name: 'Query' + (sz === 0 ? '' : sz),
+                query: '',
+                pageSize: $scope.pageSizesOptions[1].value,
+                timeLineSpan: $scope.timeLineSpans[0],
+                result: 'none',
+                rate: {
+                    value: 1,
+                    unit: 60000,
+                    installed: false
+                },
+                qryType: 'query',
+                lazy: true
+            });
+
+            $scope.addParagraph(paragraph, sz);
+
+            $timeout(() => {
+                $anchorScroll();
+
+                paragraph.ace.focus();
+            });
+        };
+
+        $scope.addScan = function() {
+            const sz = $scope.notebook.paragraphs.length;
+
+            ActivitiesData.post({ group: 'sql', action: '/queries/add/scan' });
+
+            const paragraph = _newParagraph({
+                name: 'Scan' + (sz === 0 ? '' : sz),
+                query: '',
+                pageSize: $scope.pageSizesOptions[1].value,
+                timeLineSpan: $scope.timeLineSpans[0],
+                result: 'none',
+                rate: {
+                    value: 1,
+                    unit: 60000,
+                    installed: false
+                },
+                qryType: 'scan'
+            });
+
+            $scope.addParagraph(paragraph, sz);
+        };
+
+        function _saveChartSettings(paragraph) {
+            if (!_.isEmpty(paragraph.charts)) {
+                const chart = paragraph.charts[0].api.getScope().chart;
+
+                if (!LegacyUtils.isDefined(paragraph.chartsOptions))
+                    paragraph.chartsOptions = {barChart: {stacked: true}, areaChart: {style: 'stack'}};
+
+                switch (paragraph.result) {
+                    case 'bar':
+                        paragraph.chartsOptions.barChart.stacked = chart.stacked();
+
+                        break;
+
+                    case 'area':
+                        paragraph.chartsOptions.areaChart.style = chart.style();
+
+                        break;
+
+                    default:
+                }
+            }
+        }
+
+        $scope.setResult = function(paragraph, new_result) {
+            if (paragraph.result === new_result)
+                return;
+
+            _saveChartSettings(paragraph);
+
+            paragraph.result = new_result;
+
+            if (paragraph.chart())
+                _chartApplySettings(paragraph, true);
+        };
+
+        $scope.resultEq = function(paragraph, result) {
+            return (paragraph.result === result);
+        };
+
+        $scope.paragraphExpanded = function(paragraph) {
+            const paragraph_idx = _.findIndex($scope.notebook.paragraphs, function(item) {
+                return paragraph === item;
+            });
+
+            const panel_idx = _.findIndex($scope.notebook.expandedParagraphs, function(item) {
+                return paragraph_idx === item;
+            });
+
+            return panel_idx >= 0;
+        };
+
+        const _columnFilter = function(paragraph) {
+            return paragraph.disabledSystemColumns || paragraph.systemColumns ? _allColumn : _hideColumn;
+        };
+
+        const _notObjectType = function(cls) {
+            return LegacyUtils.isJavaBuiltInClass(cls);
+        };
+
+        function _retainColumns(allCols, curCols, acceptableType, xAxis, unwantedCols) {
+            const retainedCols = [];
+
+            const availableCols = xAxis ? allCols : _.filter(allCols, function(col) {
+                return col.value >= 0;
+            });
+
+            if (availableCols.length > 0) {
+                curCols.forEach(function(curCol) {
+                    const col = _.find(availableCols, {label: curCol.label});
+
+                    if (col && acceptableType(col.type)) {
+                        col.aggFx = curCol.aggFx;
+
+                        retainedCols.push(col);
+                    }
+                });
+
+                // If nothing was restored, add first acceptable column.
+                if (_.isEmpty(retainedCols)) {
+                    let col;
+
+                    if (unwantedCols)
+                        col = _.find(availableCols, (avCol) => !_.find(unwantedCols, {label: avCol.label}) && acceptableType(avCol.type));
+
+                    if (!col)
+                        col = _.find(availableCols, (avCol) => acceptableType(avCol.type));
+
+                    if (col)
+                        retainedCols.push(col);
+                }
+            }
+
+            return retainedCols;
+        }
+
+        const _rebuildColumns = function(paragraph) {
+            _.forEach(_.groupBy(paragraph.meta, 'fieldName'), function(colsByName, fieldName) {
+                const colsByTypes = _.groupBy(colsByName, 'typeName');
+
+                const needType = _.keys(colsByTypes).length > 1;
+
+                _.forEach(colsByTypes, function(colsByType, typeName) {
+                    _.forEach(colsByType, function(col, ix) {
+                        col.fieldName = (needType && !LegacyUtils.isEmptyString(typeName) ? typeName + '.' : '') + fieldName + (ix > 0 ? ix : '');
+                    });
+                });
+            });
+
+            paragraph.gridOptions.rebuildColumns();
+
+            paragraph.chartColumns = _.reduce(paragraph.meta, (acc, col, idx) => {
+                if (_notObjectType(col.fieldTypeName)) {
+                    acc.push({
+                        label: col.fieldName,
+                        type: col.fieldTypeName,
+                        aggFx: $scope.aggregateFxs[0],
+                        value: idx.toString()
+                    });
+                }
+
+                return acc;
+            }, []);
+
+            if (paragraph.chartColumns.length > 0) {
+                paragraph.chartColumns.push(TIME_LINE);
+                paragraph.chartColumns.push(ROW_IDX);
+            }
+
+            // We could accept onl not object columns for X axis.
+            paragraph.chartKeyCols = _retainColumns(paragraph.chartColumns, paragraph.chartKeyCols, _notObjectType, true);
+
+            // We could accept only numeric columns for Y axis.
+            paragraph.chartValCols = _retainColumns(paragraph.chartColumns, paragraph.chartValCols, _numberType, false, paragraph.chartKeyCols);
+        };
+
+        $scope.toggleSystemColumns = function(paragraph) {
+            if (paragraph.disabledSystemColumns)
+                return;
+
+            paragraph.columnFilter = _columnFilter(paragraph);
+
+            paragraph.chartColumns = [];
+
+            _rebuildColumns(paragraph);
+        };
+
+        /**
+         * Execute query and get first result page.
+         *
+         * @param qryType Query type. 'query' or `scan`.
+         * @param qryArg Argument with query properties.
+         * @param {(res) => any} onQueryStarted Action to execute when query ID is received.
+         * @return {Observable<VisorQueryResult>} Observable with first query result page.
+         */
+        const _executeQuery0 = (qryType, qryArg, onQueryStarted: (res) => any = () => {}) => {
+            return from(qryType === 'scan' ? agentMgr.queryScan(qryArg) : agentMgr.querySql(qryArg)).pipe(
+                tap((res) => {
+                    onQueryStarted(res);
+                    $scope.$applyAsync();
+                }),
+                exhaustMap((res) => {
+                    if (!_.isNil(res.rows))
+                        return of(res);
+
+                    const fetchFirstPageTask = timer(100, 500).pipe(
+                        exhaustMap(() => agentMgr.queryFetchFistsPage(qryArg.nid, res.queryId, qryArg.pageSize)),
+                        filter((res) => !_.isNil(res.rows))
+                    );
+
+                    const pingQueryTask = timer(60000, 60000).pipe(
+                        exhaustMap(() => agentMgr.queryPing(qryArg.nid, res.queryId)),
+                        takeWhile(({queryPingSupported}) => queryPingSupported),
+                        ignoreElements()
+                    );
+
+                    return merge(fetchFirstPageTask, pingQueryTask);
+                }),
+                first()
+            );
+        };
+
+        /**
+         * Execute query with old query clearing and showing of query result.
+         *
+         * @param paragraph Query paragraph.
+         * @param qryArg Argument with query properties.
+         * @param {(res) => any} onQueryStarted Action to execute when query ID is received.
+         * @param {(res) => any} onQueryFinished Action to execute when first query result page is received.
+         * @param {(err) => any} onError Action to execute when error occured.
+         * @return {Observable<VisorQueryResult>} Observable with first query result page.
+         */
+        const _executeQuery = (
+            paragraph,
+            qryArg,
+            onQueryStarted: (res) => any = () => {},
+            onQueryFinished: (res) => any = () => {},
+            onError: (err) => any = () => {}
+        ) => {
+            return from(_closeOldQuery(paragraph)).pipe(
+                switchMap(() => _executeQuery0(paragraph.qryType, qryArg, onQueryStarted)),
+                tap((res) => {
+                    onQueryFinished(res);
+                    $scope.$applyAsync();
+                }),
+                takeUntil(paragraph.cancelQuerySubject),
+                catchError((err) => {
+                    onError(err);
+                    $scope.$applyAsync();
+
+                    return of(err);
+                })
+            );
+        };
+
+        /**
+         * Execute query and get all query results.
+         *
+         * @param paragraph Query paragraph.
+         * @param qryArg Argument with query properties.
+         * @param {(res) => any} onQueryStarted Action to execute when query ID is received.
+         * @param {(res) => any} onQueryFinished Action to execute when first query result page is received.
+         * @param {(err) => any} onError Action to execute when error occured.
+         * @return {Observable<any>} Observable with full query result.
+         */
+        const _exportQueryAll = (
+            paragraph,
+            qryArg,
+            onQueryStarted: (res) => any = () => {},
+            onQueryFinished: (res) => any = () => {},
+            onError: (err) => any = () => {}
+        ) => {
+            return from(_closeOldExport(paragraph)).pipe(
+                switchMap(() => _executeQuery0(paragraph.qryType, qryArg, onQueryStarted)),
+                expand((acc) => {
+                    return from(agentMgr.queryNextPage(acc.responseNodeId, acc.queryId, qryArg.pageSize)
+                        .then((res) => {
+                            acc.rows = acc.rows.concat(res.rows);
+                            acc.hasMore = res.hasMore;
+
+                            return acc;
+                        }));
+                }),
+                first((acc) => !acc.hasMore),
+                tap(onQueryFinished),
+                takeUntil(paragraph.cancelExportSubject),
+                catchError((err) => {
+                    onError(err);
+
+                    return of(err);
+                })
+            );
+        };
+
+        /**
+         * @param {Object} paragraph Query
+         * @param {Boolean} clearChart Flag is need clear chart model.
+         * @param {{columns: Array, rows: Array, responseNodeId: String, queryId: int, hasMore: Boolean}} res Query results.
+         * @private
+         */
+        const _processQueryResult = (paragraph, clearChart, res) => {
+            const prevKeyCols = paragraph.chartKeyCols;
+            const prevValCols = paragraph.chartValCols;
+
+            if (!_.eq(paragraph.meta, res.columns)) {
+                paragraph.meta = [];
+
+                paragraph.chartColumns = [];
+
+                if (!LegacyUtils.isDefined(paragraph.chartKeyCols))
+                    paragraph.chartKeyCols = [];
+
+                if (!LegacyUtils.isDefined(paragraph.chartValCols))
+                    paragraph.chartValCols = [];
+
+                if (res.columns.length) {
+                    const _key = _.find(res.columns, {fieldName: '_KEY'});
+                    const _val = _.find(res.columns, {fieldName: '_VAL'});
+
+                    paragraph.disabledSystemColumns = !(_key && _val) ||
+                        (res.columns.length === 2 && _key && _val) ||
+                        (res.columns.length === 1 && (_key || _val));
+                }
+
+                paragraph.columnFilter = _columnFilter(paragraph);
+
+                paragraph.meta = res.columns;
+
+                _rebuildColumns(paragraph);
+            }
+
+            paragraph.page = 1;
+
+            paragraph.total = 0;
+
+            paragraph.duration = res.duration;
+
+            paragraph.queryId = res.hasMore ? res.queryId : null;
+
+            paragraph.resNodeId = res.responseNodeId;
+
+            paragraph.setError({message: ''});
+
+            // Prepare explain results for display in table.
+            if (paragraph.queryArgs.query && paragraph.queryArgs.query.startsWith('EXPLAIN') && res.rows) {
+                paragraph.rows = [];
+
+                res.rows.forEach((row, i) => {
+                    const line = res.rows.length - 1 === i ? row[0] : row[0] + '\n';
+
+                    line.replace(/\"/g, '').split('\n').forEach((ln) => paragraph.rows.push([ln]));
+                });
+            }
+            else
+                paragraph.rows = res.rows;
+
+            paragraph.gridOptions.adjustHeight(paragraph.rows.length);
+
+            const chartHistory = paragraph.chartHistory;
+
+            // Clear history on query change.
+            if (clearChart) {
+                chartHistory.length = 0;
+
+                _.forEach(paragraph.charts, (chart) => chart.data.length = 0);
+            }
+
+            // Add results to history.
+            chartHistory.push({tm: new Date(), rows: paragraph.rows});
+
+            // Keep history size no more than max length.
+            while (chartHistory.length > HISTORY_LENGTH)
+                chartHistory.shift();
+
+            paragraph.showLoading(false);
+
+            if (_.isNil(paragraph.result) || paragraph.result === 'none' || paragraph.scanExplain())
+                paragraph.result = 'table';
+            else if (paragraph.chart()) {
+                let resetCharts = clearChart;
+
+                if (!resetCharts) {
+                    const curKeyCols = paragraph.chartKeyCols;
+                    const curValCols = paragraph.chartValCols;
+
+                    resetCharts = !prevKeyCols || !prevValCols ||
+                        prevKeyCols.length !== curKeyCols.length ||
+                        prevValCols.length !== curValCols.length;
+                }
+
+                _chartApplySettings(paragraph, resetCharts);
+            }
+        };
+
+        const _fetchQueryResult = (paragraph, clearChart, res) => {
+            _processQueryResult(paragraph, clearChart, res);
+            _tryStartRefresh(paragraph);
+        };
+
+        const _closeOldQuery = (paragraph) => {
+            const nid = paragraph.resNodeId;
+
+            if (paragraph.queryId) {
+                const qryId = paragraph.queryId;
+                delete paragraph.queryId;
+
+                return agentMgr.queryClose(nid, qryId);
+            }
+
+            return $q.when();
+        };
+
+        const _closeOldExport = (paragraph) => {
+            const nid = paragraph.exportNodeId;
+
+            if (paragraph.exportId) {
+                const exportId = paragraph.exportId;
+                delete paragraph.exportId;
+
+                return agentMgr.queryClose(nid, exportId);
+            }
+
+            return $q.when();
+        };
+
+        $scope.cancelQuery = (paragraph) => {
+            paragraph.cancelQuerySubject.next(true);
+
+            this.$scope.stopRefresh(paragraph);
+
+            _closeOldQuery(paragraph)
+                .catch((err) => paragraph.setError(err))
+                .finally(() => paragraph.showLoading(false));
+        };
+
+        /**
+         * @param {String} name Cache name.
+         * @param {Array.<String>} nids Cache name.
+         * @return {Promise<Array.<{nid: string, ip: string, version:string, gridName: string, os: string, client: boolean}>>}
+         */
+        const cacheNodesModel = (name, nids) => {
+            return agentMgr.topology(true)
+                .then((nodes) =>
+                    _.reduce(nodes, (acc, node) => {
+                        if (_.includes(nids, node.nodeId)) {
+                            acc.push({
+                                nid: node.nodeId.toUpperCase(),
+                                ip: _.head(node.attributes['org.apache.ignite.ips'].split(', ')),
+                                version: node.attributes['org.apache.ignite.build.ver'],
+                                gridName: node.attributes['org.apache.ignite.ignite.name'],
+                                os: `${node.attributes['os.name']} ${node.attributes['os.arch']} ${node.attributes['os.version']}`,
+                                client: node.attributes['org.apache.ignite.cache.client']
+                            });
+                        }
+
+                        return acc;
+                    }, [])
+                );
+        };
+
+        /**
+         * @param {string} name Cache name.
+         * @param {boolean} local Local query.
+         * @return {Promise<string>} Nid
+         */
+        const _chooseNode = (name, local) => {
+            if (_.isEmpty(name))
+                return Promise.resolve(null);
+
+            return agentMgr.cacheNodes(name)
+                .then((nids) => {
+                    if (local) {
+                        return cacheNodesModel(name, nids)
+                            .then((nodes) => Nodes.selectNode(nodes, name).catch(() => {}))
+                            .then((selectedNids) => _.head(selectedNids));
+                    }
+
+                    return nids[_.random(0, nids.length - 1)];
+                })
+                .catch(Messages.showError);
+        };
+
+        const _executeRefresh = (paragraph) => {
+            const args = paragraph.queryArgs;
+
+            from(agentMgr.awaitCluster()).pipe(
+                switchMap(() => args.localNid ? of(args.localNid) : from(_chooseNode(args.cacheName, false))),
+                switchMap((nid) => {
+                    paragraph.showLoading(true);
+
+                    const qryArg = {
+                        nid,
+                        cacheName: args.cacheName,
+                        query: args.query,
+                        nonCollocatedJoins: args.nonCollocatedJoins,
+                        enforceJoinOrder: args.enforceJoinOrder,
+                        replicatedOnly: false,
+                        local: !!args.localNid,
+                        pageSize: args.pageSize,
+                        lazy: args.lazy,
+                        collocated: args.collocated
+                    };
+
+                    return _executeQuery(
+                        paragraph,
+                        qryArg,
+                        (res) => _initQueryResult(paragraph, res),
+                        (res) => _fetchQueryResult(paragraph, false, res),
+                        (err) => {
+                            paragraph.setError(err);
+                            paragraph.ace && paragraph.ace.focus();
+                            $scope.stopRefresh(paragraph);
+                        }
+                    );
+                }),
+                finalize(() => paragraph.showLoading(false))
+            ).toPromise();
+        };
+
+        const _tryStartRefresh = function(paragraph) {
+            if (_.get(paragraph, 'rate.installed') && paragraph.queryExecuted() && paragraph.nonRefresh()) {
+                $scope.chartAcceptKeyColumn(paragraph, TIME_LINE);
+
+                const delay = paragraph.rate.value * paragraph.rate.unit;
+
+                paragraph.rate.stopTime = $interval(_executeRefresh, delay, 0, false, paragraph);
+            }
+        };
+
+        const addLimit = (query, limitSize) =>
+            `SELECT * FROM (
+            ${query} 
+            ) LIMIT ${limitSize}`;
+
+        $scope.nonCollocatedJoinsAvailable = () => {
+            return Version.since(this.agentMgr.clusterVersion, NON_COLLOCATED_JOINS_SINCE);
+        };
+
+        $scope.collocatedJoinsAvailable = () => {
+            return Version.since(this.agentMgr.clusterVersion, ...COLLOCATED_QUERY_SINCE);
+        };
+
+        $scope.enforceJoinOrderAvailable = () => {
+            return Version.since(this.agentMgr.clusterVersion, ...ENFORCE_JOIN_SINCE);
+        };
+
+        $scope.lazyQueryAvailable = () => {
+            return Version.since(this.agentMgr.clusterVersion, ...LAZY_QUERY_SINCE);
+        };
+
+        $scope.ddlAvailable = () => {
+            return Version.since(this.agentMgr.clusterVersion, ...DDL_SINCE);
+        };
+
+        $scope.cacheNameForSql = (paragraph) => {
+            return $scope.ddlAvailable() && !paragraph.useAsDefaultSchema ? null : paragraph.cacheName;
+        };
+
+        const _initQueryResult = (paragraph, res) => {
+            paragraph.resNodeId = res.responseNodeId;
+            paragraph.queryId = res.queryId;
+
+            if (paragraph.nonRefresh()) {
+                paragraph.rows = [];
+
+                paragraph.meta = [];
+                paragraph.setError({message: ''});
+            }
+
+            paragraph.hasNext = false;
+        };
+
+        const _initExportResult = (paragraph, res) => {
+            paragraph.exportNodeId = res.responseNodeId;
+            paragraph.exportId = res.queryId;
+        };
+
+        $scope.execute = (paragraph, local = false) => {
+            if (!$scope.queryAvailable(paragraph))
+                return;
+
+            const nonCollocatedJoins = !!paragraph.nonCollocatedJoins;
+            const enforceJoinOrder = !!paragraph.enforceJoinOrder;
+            const lazy = !!paragraph.lazy;
+            const collocated = !!paragraph.collocated;
+
+            _cancelRefresh(paragraph);
+
+            from(_chooseNode(paragraph.cacheName, local)).pipe(
+                switchMap((nid) => {
+                    // If we are executing only selected part of query then Notebook shouldn't be saved.
+                    if (!paragraph.partialQuery)
+                        Notebook.save($scope.notebook).catch(Messages.showError);
+
+                    paragraph.localQueryMode = local;
+                    paragraph.prevQuery = paragraph.queryArgs ? paragraph.queryArgs.query : paragraph.query;
+
+                    paragraph.showLoading(true);
+
+                    const query = paragraph.partialQuery || paragraph.query;
+
+                    const args = paragraph.queryArgs = {
+                        cacheName: $scope.cacheNameForSql(paragraph),
+                        query,
+                        pageSize: paragraph.pageSize,
+                        maxPages: paragraph.maxPages,
+                        nonCollocatedJoins,
+                        enforceJoinOrder,
+                        localNid: local ? nid : null,
+                        lazy,
+                        collocated
+                    };
+
+                    ActivitiesData.post({ group: 'sql', action: '/queries/execute' });
+
+                    const qry = args.maxPages ? addLimit(args.query, args.pageSize * args.maxPages) : query;
+                    const qryArg = {
+                        nid,
+                        cacheName: args.cacheName,
+                        query: qry,
+                        nonCollocatedJoins,
+                        enforceJoinOrder,
+                        replicatedOnly: false,
+                        local,
+                        pageSize: args.pageSize,
+                        lazy,
+                        collocated
+                    };
+
+                    return _executeQuery(
+                        paragraph,
+                        qryArg,
+                        (res) => _initQueryResult(paragraph, res),
+                        (res) => _fetchQueryResult(paragraph, true, res),
+                        (err) => {
+                            paragraph.setError(err);
+                            paragraph.ace && paragraph.ace.focus();
+                            $scope.stopRefresh(paragraph);
+
+                            Messages.showError(err);
+                        }
+                    );
+                }),
+                finalize(() => paragraph.showLoading(false))
+            ).toPromise();
+        };
+
+        const _cancelRefresh = (paragraph) => {
+            if (paragraph.rate && paragraph.rate.stopTime) {
+                delete paragraph.queryArgs;
+
+                _.set(paragraph, 'rate.installed', false);
+
+                $interval.cancel(paragraph.rate.stopTime);
+
+                delete paragraph.rate.stopTime;
+            }
+        };
+
+        $scope.explain = (paragraph) => {
+            if (!$scope.queryAvailable(paragraph))
+                return;
+
+            const nonCollocatedJoins = !!paragraph.nonCollocatedJoins;
+            const enforceJoinOrder = !!paragraph.enforceJoinOrder;
+            const collocated = !!paragraph.collocated;
+
+            if (!paragraph.partialQuery)
+                Notebook.save($scope.notebook).catch(Messages.showError);
+
+            _cancelRefresh(paragraph);
+
+            paragraph.showLoading(true);
+
+            from(_chooseNode(paragraph.cacheName, false)).pipe(
+                switchMap((nid) => {
+                    const qryArg = paragraph.queryArgs = {
+                        nid,
+                        cacheName: $scope.cacheNameForSql(paragraph),
+                        query: 'EXPLAIN ' + (paragraph.partialQuery || paragraph.query),
+                        nonCollocatedJoins,
+                        enforceJoinOrder,
+                        replicatedOnly: false,
+                        local: false,
+                        pageSize: paragraph.pageSize,
+                        lazy: false,
+                        collocated
+                    };
+
+                    ActivitiesData.post({ group: 'sql', action: '/queries/explain' });
+
+                    return _executeQuery(
+                        paragraph,
+                        qryArg,
+                        (res) => _initQueryResult(paragraph, res),
+                        (res) => _fetchQueryResult(paragraph, true, res),
+                        (err) => {
+                            paragraph.setError(err);
+                            paragraph.ace && paragraph.ace.focus();
+                        }
+                    );
+                }),
+                finalize(() => paragraph.showLoading(false))
+            ).toPromise();
+        };
+
+        $scope.scan = (paragraph, local = false) => {
+            if (!$scope.scanAvailable(paragraph))
+                return;
+
+            const cacheName = paragraph.cacheName;
+            const caseSensitive = !!paragraph.caseSensitive;
+            const filter = paragraph.filter;
+            const pageSize = paragraph.pageSize;
+
+            from(_chooseNode(cacheName, local)).pipe(
+                switchMap((nid) => {
+                    paragraph.localQueryMode = local;
+
+                    Notebook.save($scope.notebook)
+                        .catch(Messages.showError);
+
+                    paragraph.showLoading(true);
+
+                    const qryArg = paragraph.queryArgs = {
+                        cacheName,
+                        filter,
+                        regEx: false,
+                        caseSensitive,
+                        near: false,
+                        pageSize,
+                        localNid: local ? nid : null
+                    };
+
+                    qryArg.nid = nid;
+                    qryArg.local = local;
+
+                    ActivitiesData.post({ group: 'sql', action: '/queries/scan' });
+
+                    return _executeQuery(
+                        paragraph,
+                        qryArg,
+                        (res) => _initQueryResult(paragraph, res),
+                        (res) => _fetchQueryResult(paragraph, true, res),
+                        (err) => paragraph.setError(err)
+                    );
+                }),
+                finalize(() => paragraph.showLoading(false))
+            ).toPromise();
+        };
+
+        function _updatePieChartsWithData(paragraph, newDatum) {
+            $timeout(() => {
+                _.forEach(paragraph.charts, function(chart) {
+                    const chartDatum = chart.data;
+
+                    chartDatum.length = 0;
+
+                    _.forEach(newDatum, function(series) {
+                        if (chart.options.title.text === series.key)
+                            _.forEach(series.values, (v) => chartDatum.push(v));
+                    });
+                });
+
+                _.forEach(paragraph.charts, (chart) => chart.api.update());
+            });
+        }
+
+        const _processQueryNextPage = (paragraph, res) => {
+            paragraph.page++;
+
+            paragraph.total += paragraph.rows.length;
+
+            paragraph.duration = res.duration;
+
+            paragraph.rows = res.rows;
+
+            if (paragraph.chart()) {
+                if (paragraph.result === 'pie')
+                    _updatePieChartsWithData(paragraph, _pieChartDatum(paragraph));
+                else
+                    _updateChartsWithData(paragraph, _chartDatum(paragraph));
+            }
+
+            paragraph.gridOptions.adjustHeight(paragraph.rows.length);
+
+            paragraph.showLoading(false);
+
+            if (!res.hasMore)
+                delete paragraph.queryId;
+        };
+
+        $scope.nextPage = (paragraph) => {
+            paragraph.showLoading(true);
+
+            paragraph.queryArgs.pageSize = paragraph.pageSize;
+
+            const nextPageTask = from(agentMgr.queryNextPage(paragraph.resNodeId, paragraph.queryId, paragraph.pageSize)
+                .then((res) => _processQueryNextPage(paragraph, res))
+                .catch((err) => {
+                    paragraph.setError(err);
+                    paragraph.ace && paragraph.ace.focus();
+                }));
+
+            const pingQueryTask = timer(60000, 60000).pipe(
+                exhaustMap(() => agentMgr.queryPing(paragraph.resNodeId, paragraph.queryId)),
+                takeWhile(({queryPingSupported}) => queryPingSupported),
+                ignoreElements()
+            );
+
+            merge(nextPageTask, pingQueryTask).pipe(
+                take(1),
+                takeUntil(paragraph.cancelQuerySubject)
+            ).subscribe();
+        };
+
+        const _export = (fileName, columnDefs, meta, rows, toClipBoard = false) => {
+            const csvSeparator = this.CSV.getSeparator();
+            let csvContent = '';
+
+            const cols = [];
+            const excludedCols = [];
+
+            _.forEach(meta, (col, idx) => {
+                if (columnDefs[idx].visible)
+                    cols.push(_fullColName(col));
+                else
+                    excludedCols.push(idx);
+            });
+
+            csvContent += cols.join(csvSeparator) + '\n';
+
+            _.forEach(rows, (row) => {
+                cols.length = 0;
+
+                if (Array.isArray(row)) {
+                    _.forEach(row, (elem, idx) => {
+                        if (_.includes(excludedCols, idx))
+                            return;
+
+                        cols.push(_.isUndefined(elem) ? '' : JSON.stringify(elem));
+                    });
+                }
+                else {
+                    _.forEach(columnDefs, (col) => {
+                        if (col.visible) {
+                            const elem = row[col.fieldName];
+
+                            cols.push(_.isUndefined(elem) ? '' : JSON.stringify(elem));
+                        }
+                    });
+                }
+
+                csvContent += cols.join(csvSeparator) + '\n';
+            });
+
+            if (toClipBoard)
+                IgniteCopyToClipboard.copy(csvContent);
+            else
+                LegacyUtils.download('text/csv', fileName, csvContent);
+        };
+
+        /**
+         * Generate file name with query results.
+         *
+         * @param paragraph {Object} Query paragraph .
+         * @param all {Boolean} All result export flag.
+         * @returns {string}
+         */
+        const exportFileName = (paragraph, all) => {
+            const args = paragraph.queryArgs;
+
+            if (paragraph.qryType === 'scan')
+                return `export-scan-${args.cacheName}-${paragraph.name}${all ? '-all' : ''}.csv`;
+
+            return `export-query-${paragraph.name}${all ? '-all' : ''}.csv`;
+        };
+
+        $scope.exportCsvToClipBoard = (paragraph) => {
+            _export(exportFileName(paragraph, false), paragraph.gridOptions.columnDefs, paragraph.meta, paragraph.rows, true);
+        };
+
+        $scope.exportCsv = function(paragraph) {
+            _export(exportFileName(paragraph, false), paragraph.gridOptions.columnDefs, paragraph.meta, paragraph.rows);
+
+            // paragraph.gridOptions.api.exporter.csvExport(uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE);
+        };
+
+        $scope.exportPdf = function(paragraph) {
+            paragraph.gridOptions.api.exporter.pdfExport(uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE);
+        };
+
+        $scope.exportCsvAll = (paragraph) => {
+            const args = paragraph.queryArgs;
+
+            paragraph.cancelExportSubject.next(true);
+
+            paragraph.csvIsPreparing = true;
+
+            return (args.localNid ? of(args.localNid) : from(_chooseNode(args.cacheName, false))).pipe(
+                map((nid) => _.assign({}, args, {nid, pageSize: 1024, local: !!args.localNid, replicatedOnly: false})),
+                switchMap((arg) => _exportQueryAll(
+                    paragraph,
+                    arg,
+                    (res) => _initExportResult(paragraph, res),
+                    (res) => _export(exportFileName(paragraph, true), paragraph.gridOptions.columnDefs, res.columns, res.rows),
+                    (err) => {
+                        Messages.showError(err);
+                        return of(err);
+                    }
+                )),
+                finalize(() => paragraph.csvIsPreparing = false)
+            ).toPromise();
+        };
+
+        // $scope.exportPdfAll = function(paragraph) {
+        //    $http.post('/api/v1/agent/query/getAll', {query: paragraph.query, cacheName: paragraph.cacheName})
+        //    .then(({data}) {
+        //        _export(paragraph.name + '-all.csv', data.meta, data.rows);
+        //    })
+        //    .catch(Messages.showError);
+        // };
+
+        $scope.rateAsString = function(paragraph) {
+            if (paragraph.rate && paragraph.rate.installed) {
+                const idx = _.findIndex($scope.timeUnit, function(unit) {
+                    return unit.value === paragraph.rate.unit;
+                });
+
+                if (idx >= 0)
+                    return ' ' + paragraph.rate.value + $scope.timeUnit[idx].short;
+
+                paragraph.rate.installed = false;
+            }
+
+            return '';
+        };
+
+        $scope.startRefresh = function(paragraph, value, unit) {
+            $scope.stopRefresh(paragraph);
+
+            paragraph.rate.value = value;
+            paragraph.rate.unit = unit;
+            paragraph.rate.installed = true;
+
+            if (paragraph.queryExecuted() && !paragraph.scanExplain())
+                _executeRefresh(paragraph);
+        };
+
+        $scope.stopRefresh = function(paragraph) {
+            _.set(paragraph, 'rate.installed', false);
+
+            _tryStopRefresh(paragraph);
+        };
+
+        $scope.paragraphTimeSpanVisible = function(paragraph) {
+            return paragraph.timeLineSupported() && paragraph.chartTimeLineEnabled();
+        };
+
+        $scope.paragraphTimeLineSpan = function(paragraph) {
+            if (paragraph && paragraph.timeLineSpan)
+                return paragraph.timeLineSpan.toString();
+
+            return '1';
+        };
+
+        $scope.applyChartSettings = function(paragraph) {
+            _chartApplySettings(paragraph, true);
+        };
+
+        $scope.queryAvailable = function(paragraph) {
+            return paragraph.query && !paragraph.loading;
+        };
+
+        $scope.queryTooltip = function(paragraph, action) {
+            if ($scope.queryAvailable(paragraph))
+                return;
+
+            if (paragraph.loading)
+                return 'Waiting for server response';
+
+            return 'Input text to ' + action;
+        };
+
+        $scope.scanAvailable = function(paragraph) {
+            return $scope.caches.length && !(paragraph.loading || paragraph.csvIsPreparing);
+        };
+
+        $scope.scanTooltip = function(paragraph) {
+            if ($scope.scanAvailable(paragraph))
+                return;
+
+            if (paragraph.loading)
+                return 'Waiting for server response';
+
+            return 'Select cache to export scan results';
+        };
+
+        $scope.clickableMetadata = function(node) {
+            return node.type.slice(0, 5) !== 'index';
+        };
+
+        $scope.dblclickMetadata = function(paragraph, node) {
+            paragraph.ace.insert(node.name);
+
+            setTimeout(() => paragraph.ace.focus(), 100);
+        };
+
+        $scope.importMetadata = function() {
+            Loading.start('loadingCacheMetadata');
+
+            $scope.metadata = [];
+
+            agentMgr.metadata()
+                .then((metadata) => {
+                    $scope.metadata = _.sortBy(_.filter(metadata, (meta) => {
+                        const cache = _.find($scope.caches, { value: meta.cacheName });
+
+                        if (cache) {
+                            meta.name = (cache.sqlSchema || '"' + meta.cacheName + '"') + '.' + meta.typeName;
+                            meta.displayName = (cache.sqlSchema || meta.maskedName) + '.' + meta.typeName;
+
+                            if (cache.sqlSchema)
+                                meta.children.unshift({type: 'plain', name: 'cacheName: ' + meta.maskedName, maskedName: meta.maskedName});
+
+                            meta.children.unshift({type: 'plain', name: 'mode: ' + cache.mode, maskedName: meta.maskedName});
+                        }
+
+                        return cache;
+                    }), 'name');
+                })
+                .catch(Messages.showError)
+                .then(() => Loading.finish('loadingCacheMetadata'));
+        };
+
+        $scope.showResultQuery = function(paragraph) {
+            if (!_.isNil(paragraph)) {
+                const scope = $scope.$new();
+
+                if (paragraph.qryType === 'scan') {
+                    scope.title = 'SCAN query';
+
+                    const filter = paragraph.queryArgs.filter;
+
+                    if (_.isEmpty(filter))
+                        scope.content = [`SCAN query for cache: <b>${maskCacheName(paragraph.queryArgs.cacheName, true)}</b>`];
+                    else
+                        scope.content = [`SCAN query for cache: <b>${maskCacheName(paragraph.queryArgs.cacheName, true)}</b> with filter: <b>${filter}</b>`];
+                }
+                else if (paragraph.queryArgs.query.startsWith('EXPLAIN ')) {
+                    scope.title = 'Explain query';
+                    scope.content = paragraph.queryArgs.query.split(/\r?\n/);
+                }
+                else {
+                    scope.title = 'SQL query';
+                    scope.content = paragraph.queryArgs.query.split(/\r?\n/);
+                }
+
+                // Attach duration and selected node info
+                scope.meta = `Duration: ${$filter('duration')(paragraph.duration)}.`;
+                scope.meta += paragraph.localQueryMode ? ` Node ID8: ${id8(paragraph.resNodeId)}` : '';
+
+                // Show a basic modal from a controller
+                $modal({scope, templateUrl: messageTemplateUrl, show: true});
+            }
+        };
+
+        $scope.showStackTrace = function(paragraph) {
+            if (!_.isNil(paragraph)) {
+                const scope = $scope.$new();
+
+                scope.title = 'Error details';
+                scope.content = [];
+
+                const tab = '&nbsp;&nbsp;&nbsp;&nbsp;';
+
+                const addToTrace = (item) => {
+                    if (nonNil(item)) {
+                        scope.content.push((scope.content.length > 0 ? tab : '') + errorParser.extractFullMessage(item));
+
+                        addToTrace(item.cause);
+
+                        _.forEach(item.suppressed, (sup) => addToTrace(sup));
+                    }
+                };
+
+                addToTrace(paragraph.error.root);
+
+                // Show a basic modal from a controller
+                $modal({scope, templateUrl: messageTemplateUrl, show: true});
+            }
+        };
+
+        this.offTransitions = $transitions.onBefore({from: 'base.sql.notebook'}, ($transition$) => {
+            const options = $transition$.options();
+
+            // Skip query closing in case of auto redirection on state change.
+            if (options.redirectedFrom)
+                return true;
+
+            return this.closeOpenedQueries();
+        });
+
+        $window.addEventListener('beforeunload', this.closeOpenedQueries);
+
+        this.onClusterSwitchLnr = () => {
+            const paragraphs = _.get(this, '$scope.notebook.paragraphs');
+
+            if (this._hasRunningQueries(paragraphs)) {
+                try {
+                    return Confirm.confirm('You have running queries. Are you sure you want to cancel them?')
+                        .then(() => this._closeOpenedQueries(paragraphs));
+                }
+                catch (err) {
+                    return Promise.reject(new CancellationError());
+                }
+            }
+
+            return Promise.resolve(true);
+        };
+
+        agentMgr.addClusterSwitchListener(this.onClusterSwitchLnr);
+    }
+
+    _closeOpenedQueries(paragraphs) {
+        return Promise.all(_.map(paragraphs, (paragraph) => {
+            paragraph.cancelQuerySubject.next(true);
+            paragraph.cancelExportSubject.next(true);
+
+            return Promise.all([paragraph.queryId
+                ? this.agentMgr.queryClose(paragraph.resNodeId, paragraph.queryId)
+                    .catch(() => Promise.resolve(true))
+                    .finally(() => delete paragraph.queryId)
+                : Promise.resolve(true),
+            paragraph.csvIsPreparing && paragraph.exportId
+                ? this.agentMgr.queryClose(paragraph.exportNodeId, paragraph.exportId)
+                    .catch(() => Promise.resolve(true))
+                    .finally(() => delete paragraph.exportId)
+                : Promise.resolve(true)]
+            );
+        }));
+    }
+
+    _hasRunningQueries(paragraphs) {
+        return !!_.find(paragraphs,
+            (paragraph) => paragraph.loading || paragraph.scanningInProgress || paragraph.csvIsPreparing);
+    }
+
+    async closeOpenedQueries() {
+        const paragraphs = _.get(this, '$scope.notebook.paragraphs');
+
+        if (this._hasRunningQueries(paragraphs)) {
+            try {
+                await this.Confirm.confirm('You have running queries. Are you sure you want to cancel them?');
+                this._closeOpenedQueries(paragraphs);
+
+                return true;
+            }
+            catch (ignored) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    scanActions: QueryActions<Paragraph & {type: 'scan'}> = [
+        {
+            text: 'Scan',
+            click: (p) => this.$scope.scan(p),
+            available: (p) => this.$scope.scanAvailable(p)
+        },
+        {
+            text: 'Scan on selected node',
+            click: (p) => this.$scope.scan(p, true),
+            available: (p) => this.$scope.scanAvailable(p)
+        },
+        {text: 'Rename', click: (p) => this.renameParagraph(p), available: () => true},
+        {text: 'Remove', click: (p) => this.removeParagraph(p), available: () => true}
+    ];
+
+    queryActions: QueryActions<Paragraph & {type: 'query'}> = [
+        {
+            text: 'Execute',
+            click: (p) => this.$scope.execute(p),
+            available: (p) => this.$scope.queryAvailable(p)
+        },
+        {
+            text: 'Execute on selected node',
+            click: (p) => this.$scope.execute(p, true),
+            available: (p) => this.$scope.queryAvailable(p)
+        },
+        {
+            text: 'Explain',
+            click: (p) => this.$scope.explain(p),
+            available: (p) => this.$scope.queryAvailable(p)
+        },
+        {text: 'Rename', click: (p) => this.renameParagraph(p), available: () => true},
+        {text: 'Remove', click: (p) => this.removeParagraph(p), available: () => true}
+    ];
+
+    async renameParagraph(paragraph: Paragraph) {
+        try {
+            const newName = await this.IgniteInput.input('Rename Query', 'New query name:', paragraph.name);
+
+            if (paragraph.name !== newName) {
+                paragraph.name = newName;
+
+                this.$scope.rebuildScrollParagraphs();
+
+                await this.Notebook.save(this.$scope.notebook)
+                    .catch(this.Messages.showError);
+            }
+        }
+        catch (ignored) {
+            // No-op.
+        }
+    }
+
+    async removeParagraph(paragraph: Paragraph) {
+        try {
+            const msg = (this._hasRunningQueries([paragraph])
+                ? 'Query is being executed. Are you sure you want to cancel and remove query: "'
+                : 'Are you sure you want to remove query: "') + paragraph.name + '"?';
+
+            await this.Confirm.confirm(msg);
+
+            this.$scope.stopRefresh(paragraph);
+            this._closeOpenedQueries([paragraph]);
+
+            const paragraph_idx = _.findIndex(this.$scope.notebook.paragraphs, (item) => paragraph === item);
+            const panel_idx = _.findIndex(this.$scope.expandedParagraphs, (item) => paragraph_idx === item);
+
+            if (panel_idx >= 0)
+                this.$scope.expandedParagraphs.splice(panel_idx, 1);
+
+            this.$scope.notebook.paragraphs.splice(paragraph_idx, 1);
+            this.$scope.rebuildScrollParagraphs();
+
+            paragraph.cancelQuerySubject.complete();
+            paragraph.cancelExportSubject.complete();
+
+            await this.Notebook.save(this.$scope.notebook)
+                .catch(this.Messages.showError);
+        }
+        catch (ignored) {
+            // No-op.
+        }
+    }
+
+    isParagraphOpened(index: number) {
+        return this.$scope.notebook.expandedParagraphs.includes(index);
+    }
+
+    onParagraphClose(index: number) {
+        const expanded = this.$scope.notebook.expandedParagraphs;
+        expanded.splice(expanded.indexOf(index), 1);
+    }
+
+    onParagraphOpen(index: number) {
+        this.$scope.notebook.expandedParagraphs.push(index);
+    }
+
+    $onDestroy() {
+        if (this.subscribers$)
+            this.subscribers$.unsubscribe();
+
+        if (this.offTransitions)
+            this.offTransitions();
+
+        this.agentMgr.removeClusterSwitchListener(this.onClusterSwitchLnr);
+        this.$window.removeEventListener('beforeunload', this.closeOpenedQueries);
+    }
+}
diff --git a/modules/frontend/app/components/page-queries/components/queries-notebook/index.js b/modules/frontend/app/components/page-queries/components/queries-notebook/index.js
new file mode 100644
index 0000000..42f2d07
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/components/queries-notebook/index.js
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import templateUrl from './template.tpl.pug';
+import {NotebookCtrl} from './controller';
+import NotebookData from '../../notebook.data';
+import {component as actions} from './components/query-actions-button/component';
+import {default as igniteInformation} from './components/ignite-information/information.directive';
+import './style.scss';
+
+export default angular.module('ignite-console.sql.notebook', [])
+    .directive('igniteInformation', igniteInformation)
+    .component('queryActionsButton', actions)
+    .component('queriesNotebook', {
+        controller: NotebookCtrl,
+        templateUrl
+    })
+    .service('IgniteNotebookData', NotebookData);
diff --git a/modules/frontend/app/components/page-queries/components/queries-notebook/style.scss b/modules/frontend/app/components/page-queries/components/queries-notebook/style.scss
new file mode 100644
index 0000000..8645316
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/components/queries-notebook/style.scss
@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import "../../../../../public/stylesheets/variables.scss";
+
+queries-notebook {
+    // TODO: Delete this breadcrumbs styles after template refactoring to new design.
+    .notebooks-top {
+        display: flex;
+        align-items: center;
+        margin: 40px 0 30px;
+        flex-direction: row;
+
+        h1 {
+            margin: 0 !important;
+            display: flex;
+            align-items: center;
+            align-content: center;
+
+            label {
+                font-size: 24px;
+                margin-right: 10px;
+            }
+        }
+
+        breadcrumbs {
+            padding-left: 0;
+
+            .breadcrumbs__home {
+                margin-left: 0 !important;
+            }
+        }
+
+        cluster-selector {
+            margin-left: 30px;
+            display: inline-flex;
+        }
+    }
+
+    .block-information {
+        padding: 18px 10px 18px 36px;
+
+        [ng-transclude] {
+            display: flex;
+        }
+
+        [ignite-icon] {
+            top: 18px;
+            left: 14px;
+            width: 14px;
+            color: inherit;
+        }
+
+        ul {
+            min-width: 300px;
+        }
+
+        .example {
+            flex-basis: 100%;
+
+            .group {
+                background: white;
+                border-style: solid;
+
+                .group-legend {
+                    label {
+                        @import "public/stylesheets/variables.scss";
+
+                        font-size: 12px;
+                        color: $gray-light;
+                        background: #fcfcfc;
+                        padding: 0;
+                        vertical-align: 1px;
+                    }
+                }
+
+                .group-content {
+                    height: 66px;
+                    border-radius: 5px;
+
+                    margin: 0;
+                    padding: 5px 0;
+
+                    overflow: hidden;
+                }
+            }
+        }
+
+        .sql-editor {
+            width: 100%;
+            margin: 0;
+        }
+    }
+
+    .empty-caches {
+        display: flex;
+        padding: 10px;
+        align-items: center;
+        justify-content: center;
+        text-align: center;
+    }
+
+    .notebook-top-buttons {
+        display: flex;
+        align-items: center;
+        margin-left: auto;
+
+        button:first-of-type {
+            margin-right: 20px;
+        }
+
+        a {
+            color: rgb(57, 57, 57);
+        }
+    }
+
+    .btn.btn-default.select-toggle.tipLabel {
+        padding-right: 25px;
+    }
+
+    .form-field__sensitive {
+        input[type='checkbox'] {
+            display: none;
+        }
+
+        input:checked + span {
+            color: #0067b9;
+        }
+    }
+
+    .queries-notebook-displayed-caches {
+        max-height: 210px;
+        padding: 0 5px;
+        margin-top: 10px;
+        margin-left: -5px;
+        margin-right: -5px;
+
+        overflow-y: auto;
+    }
+
+    .notebook-failed--block {
+        text-align: center;
+        display: flex;
+        min-height: 200px;
+        align-items: center;
+        justify-content: center;
+        margin-bottom: 30px;
+        background: white;
+        box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
+    }
+}
+
+.popover.settings.refresh-rate {
+    width: 244px;
+
+    [ignite-icon] {
+        height: 12px;
+    }
+
+    .popover-title {
+        padding: 10px;
+        font-size: 14px;
+    }
+
+    .actions {
+        width: 100%;
+        text-align: right;
+
+        button {
+            margin-top: 20px;
+            margin-right: 0;
+        }
+    }
+
+    .ignite-form-field {
+        display: flex;
+        padding: 5px;
+
+        input {
+            margin-right: 10px;
+        }
+    }
+}
diff --git a/modules/frontend/app/components/page-queries/components/queries-notebook/template.tpl.pug b/modules/frontend/app/components/page-queries/components/queries-notebook/template.tpl.pug
new file mode 100644
index 0000000..60a2ddf
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/components/queries-notebook/template.tpl.pug
@@ -0,0 +1,510 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+mixin form-field__sensitive({ label, modelFilter, modelSensitive, name, placeholder })
+    .form-field.form-field__sensitive.ignite-form-field
+        +form-field__label({ label, name })
+            +form-field__tooltip({ title: 'You can set case sensitive search' })
+        .form-field__control.form-field__control-group
+            +form-field__input({ name, model: modelFilter, placeholder })(
+                type='text'
+            )
+            label.btn-ignite.btn-ignite--secondary
+                +form-field__input({ name: `${ name } + "Sensitive"`, model: modelSensitive, placeholder })(
+                    type='checkbox'
+                )
+                span Cs
+        .form-field__errors(
+            ng-messages=`$input.$error`
+            ng-show=`($input.$dirty || $input.$touched || $input.$submitted) && $input.$invalid`
+        )
+
+mixin btn-toolbar(btn, click, tip, focusId)
+    i.btn.btn-default.fa(class=btn ng-click=click bs-tooltip='' data-title=tip ignite-on-click-focus=focusId data-trigger='hover' data-placement='bottom')
+
+mixin btn-toolbar-data(btn, kind, tip)
+    i.btn.btn-default.fa(class=btn ng-click=`setResult(paragraph, '${kind}')` ng-class=`{active: resultEq(paragraph, '${kind}')}` bs-tooltip='' data-title=tip data-trigger='hover' data-placement='bottom')
+
+mixin result-toolbar
+    .btn-group(ng-model='paragraph.result' ng-click='$event.stopPropagation()' style='left: 50%; margin: 0 0 0 -70px;display: block;')
+        +btn-toolbar-data('fa-table', 'table', 'Show data in tabular form')
+        +btn-toolbar-data('fa-bar-chart', 'bar', 'Show bar chart<br/>By default first column - X values, second column - Y values<br/>In case of one column it will be treated as Y values')
+        +btn-toolbar-data('fa-pie-chart', 'pie', 'Show pie chart<br/>By default first column - pie labels, second column - pie values<br/>In case of one column it will be treated as pie values')
+        +btn-toolbar-data('fa-line-chart', 'line', 'Show line chart<br/>By default first column - X values, second column - Y values<br/>In case of one column it will be treated as Y values')
+        +btn-toolbar-data('fa-area-chart', 'area', 'Show area chart<br/>By default first column - X values, second column - Y values<br/>In case of one column it will be treated as Y values')
+
+mixin chart-settings
+    .total.row
+        .col-xs-7
+            .chart-settings-link(ng-show='paragraph.chart && paragraph.chartColumns.length > 0')
+                a(title='Click to show chart settings dialog' ng-click='$event.stopPropagation()' bs-popover data-template-url='{{ $ctrl.chartSettingsTemplateUrl }}' data-placement='bottom' data-auto-close='1' data-trigger='click')
+                    i.fa.fa-bars
+                    | Chart settings
+                div(ng-show='paragraphTimeSpanVisible(paragraph)')
+                    label Show
+                    button.select-manual-caret.btn.btn-default(ng-model='paragraph.timeLineSpan' ng-change='applyChartSettings(paragraph)' bs-options='item for item in timeLineSpans' bs-select data-caret-html='<span class="caret"></span>')
+                    label min
+
+                div
+                    label Duration: #[b {{paragraph.duration | duration}}]
+                    label.margin-left-dflt(ng-show='paragraph.localQueryMode') NodeID8: #[b {{paragraph.resNodeId | id8}}]
+        .col-xs-2
+            +result-toolbar
+
+mixin query-settings
+    div
+        .form-field--inline(
+            bs-tooltip
+            data-placement='top'
+            data-title='Max number of rows to show in query result as one page'
+        )
+            +form-field__dropdown({
+                label: 'Rows per page:',
+                model: 'paragraph.pageSize',
+                name: '"pageSize" + paragraph.id',
+                options: 'pageSizesOptions'
+            })
+
+        .form-field--inline(
+            bs-tooltip
+            data-placement='top'
+            data-title='Limit query max results to specified number of pages'
+        )
+            +form-field__dropdown({
+                label: 'Max pages:',
+                model: 'paragraph.maxPages',
+                name: '"maxPages" + paragraph.id',
+                options: 'maxPages'
+            })
+
+        .form-field--inline(
+            bs-tooltip
+            data-placement='bottom'
+            data-title='Configure periodical execution of last successfully executed query'
+        )
+            button.btn-ignite-group(
+                bs-popover
+                data-template-url='{{ $ctrl.paragraphRateTemplateUrl }}'
+                data-placement='bottom-right'
+                data-auto-close='1'
+                data-trigger='click'
+            )
+                .btn-ignite(
+                    ng-class='{\
+                        "btn-ignite--primary": paragraph.rate && paragraph.rate.installed,\
+                        "btn-ignite--secondary": !(paragraph.rate && paragraph.rate.installed),\
+                    }'
+                )
+                    svg(ignite-icon='clock')
+                    | &nbsp; {{ rateAsString(paragraph) }}
+                .btn-ignite(
+                    ng-class='{\
+                        "btn-ignite--primary": paragraph.rate && paragraph.rate.installed,\
+                        "btn-ignite--secondary": !(paragraph.rate && paragraph.rate.installed),\
+                    }'
+                )
+                    span.icon.fa.fa-caret-down
+    div
+        .row(ng-if='nonCollocatedJoinsAvailable(paragraph)')
+            +form-field__checkbox({
+                label: 'Allow non-collocated joins',
+                model: 'paragraph.nonCollocatedJoins',
+                name: '"nonCollocatedJoins" + paragraph.id',
+                tip: 'Non-collocated joins is a special mode that allow to join data across cluster without collocation.<br/>\
+                Nested joins are not supported for now.<br/>\
+                <b>NOTE</b>: In some cases it may consume more heap memory or may take a long time than collocated joins.',
+                tipOpts: { placement: 'top' }
+            })
+
+        .row(ng-if='collocatedJoinsAvailable(paragraph)')
+            +form-field__checkbox({
+                label: 'Collocated Query',
+                model: 'paragraph.collocated',
+                name: '"collocated" + paragraph.id',
+                tip: 'Used For Optimization Purposes Of Queries With GROUP BY Statements.<br/>\
+                <b>NOTE:</b> Whenever Ignite executes a distributed query, it sends sub-queries to individual cluster members.<br/>\
+                If you know in advance that the elements of your query selection are collocated together on the same node\
+                and you group by collocated key (primary or affinity key), then Ignite can make significant performance and\
+                network optimizations by grouping data on remote nodes.',
+                tipOpts: { placement: 'top' }
+            })
+
+        .row(ng-if='enforceJoinOrderAvailable(paragraph)')
+            +form-field__checkbox({
+                label: 'Enforce join order',
+                model: 'paragraph.enforceJoinOrder',
+                name: '"enforceJoinOrder" + paragraph.id',
+                tip: 'Enforce join order of tables in the query.<br/>\
+                If <b>set</b>, then query optimizer will not reorder tables within join.<br/>\
+                <b>NOTE:</b> It is not recommended to enable this property unless you have verified that\
+                indexes are not selected in optimal order.',
+                tipOpts: { placement: 'top' }
+            })
+
+        .row(ng-if='lazyQueryAvailable(paragraph)')
+            +form-field__checkbox({
+                label: 'Lazy result set',
+                model: 'paragraph.lazy',
+                name: '"lazy" + paragraph.id',
+                tip: 'By default Ignite attempts to fetch the whole query result set to memory and send it to the client.<br/>\
+                For small and medium result sets this provides optimal performance and minimize duration of internal database locks, thus increasing concurrency.<br/>\
+                If result set is too big to fit in available memory this could lead to excessive GC pauses and even OutOfMemoryError.<br/>\
+                Use this flag as a hint for Ignite to fetch result set lazily, thus minimizing memory consumption at the cost of moderate performance hit.',
+                tipOpts: { placement: 'top' }
+            })
+
+mixin query-actions
+    button.btn-ignite.btn-ignite--primary(ng-disabled='!queryAvailable(paragraph)' ng-click='execute(paragraph)')
+        span.icon-left.fa.fa-fw.fa-play(ng-hide='paragraph.executionInProgress(false)')
+        span.icon-left.fa.fa-fw.fa-refresh.fa-spin(ng-show='paragraph.executionInProgress(false)')
+        | Execute
+
+    button.btn-ignite.btn-ignite--primary(ng-disabled='!queryAvailable(paragraph)' ng-click='execute(paragraph, true)')
+        span.icon-left.fa.fa-fw.fa-play(ng-hide='paragraph.executionInProgress(true)')
+        span.icon-left.fa.fa-fw.fa-refresh.fa-spin(ng-show='paragraph.executionInProgress(true)')
+        | Execute on selected node
+
+    button.btn-ignite.btn-ignite--secondary(ng-disabled='!queryAvailable(paragraph)' ng-click='explain(paragraph)' data-placement='bottom' bs-tooltip='' data-title='{{queryTooltip(paragraph, "explain query")}}')
+        | Explain
+
+    button.btn-ignite.btn-ignite--secondary(ng-if='paragraph.executionInProgress(false) || paragraph.executionInProgress(true)' ng-click='cancelQuery(paragraph)' data-placement='bottom' bs-tooltip='' data-title='{{"Cancel query execution"}}')
+        | Cancel
+
+mixin table-result-heading-query
+    .total.row
+        .col-xs-7
+            grid-column-selector(grid-api='paragraph.gridOptions.api')
+                .fa.fa-bars.icon
+            label Page: #[b {{paragraph.page}}]
+            label.margin-left-dflt Results so far: #[b {{paragraph.rows.length + paragraph.total}}]
+            label.margin-left-dflt Duration: #[b {{paragraph.duration | duration}}]
+            label.margin-left-dflt(ng-show='paragraph.localQueryMode') NodeID8: #[b {{paragraph.resNodeId | id8}}]
+        .col-xs-2
+            div(ng-if='paragraph.qryType === "query"')
+                +result-toolbar
+        .col-xs-3
+            .pull-right
+                .btn-ignite-group
+                    button.btn-ignite.btn-ignite--primary(
+                        ng-click='exportCsv(paragraph)'
+                        ng-disabled='paragraph.loading'
+                        bs-tooltip=''
+                        ng-attr-title='{{ queryTooltip(paragraph, "export query results") }}'
+                        data-trigger='hover'
+                        data-placement='bottom'
+                    )
+                        svg(ignite-icon='csv' ng-if='!paragraph.csvIsPreparing')
+                        svg.fa-spin(ignite-icon='refresh' ng-if='paragraph.csvIsPreparing')
+                        |  &nbsp; Export
+
+                    -var options = [{ text: 'Export', click: 'exportCsv(paragraph)' }, { text: 'Export all', click: 'exportCsvAll(paragraph)' }, { divider: true }, { text: '<span title="Copy current result page to clipboard">Copy to clipboard</span>', click: 'exportCsvToClipBoard(paragraph)' }]
+                    button.btn-ignite.btn-ignite--primary(
+                        ng-disabled='paragraph.loading'
+                        bs-dropdown=`${JSON.stringify(options)}`
+                        data-toggle='dropdown'
+                        data-container='body'
+                        data-placement='bottom-right'
+                        data-html='true'
+                    )
+                        span.icon.fa.fa-caret-down
+
+
+mixin table-result-heading-scan
+    .total.row
+        .col-xs-7
+            grid-column-selector(grid-api='paragraph.gridOptions.api')
+                .fa.fa-bars.icon
+            label Page: #[b {{paragraph.page}}]
+            label.margin-left-dflt Results so far: #[b {{paragraph.rows.length + paragraph.total}}]
+            label.margin-left-dflt Duration: #[b {{paragraph.duration | duration}}]
+            label.margin-left-dflt(ng-show='paragraph.localQueryMode') NodeID8: #[b {{paragraph.resNodeId | id8}}]
+        .col-xs-2
+            div(ng-if='paragraph.qryType === "query"')
+                +result-toolbar
+        .col-xs-3
+            .pull-right
+                .btn-group.panel-tip-container
+                    // TODO: replace this logic for exporting under one component
+                    button.btn.btn-primary.btn--with-icon(
+                        ng-click='exportCsv(paragraph)'
+
+                        ng-disabled='paragraph.loading || paragraph.csvIsPreparing'
+
+                        bs-tooltip=''
+                        ng-attr-title='{{ scanTooltip(paragraph) }}'
+
+                        data-trigger='hover'
+                        data-placement='bottom'
+                    )
+                        svg(ignite-icon='csv' ng-if='!paragraph.csvIsPreparing')
+                        i.fa.fa-fw.fa-refresh.fa-spin(ng-if='paragraph.csvIsPreparing')
+                        span Export
+
+                    -var options = [{ text: "Export", click: 'exportCsv(paragraph)' }, { text: 'Export all', click: 'exportCsvAll(paragraph)' }, { divider: true }, { text: '<span title="Copy current result page to clipboard">Copy to clipboard</span>', click: 'exportCsvToClipBoard(paragraph)' }]
+                    button.btn.dropdown-toggle.btn-primary(
+                        ng-disabled='paragraph.loading || paragraph.csvIsPreparing'
+
+                        bs-dropdown=`${JSON.stringify(options)}`
+
+                        data-toggle='dropdown'
+                        data-container='body'
+                        data-placement='bottom-right'
+                        data-html='true'
+                    )
+                        span.caret
+
+mixin table-result-body
+    .grid(ui-grid='paragraph.gridOptions' ui-grid-resize-columns ui-grid-exporter ui-grid-hovering)
+
+mixin chart-result
+    div(ng-hide='paragraph.scanExplain()')
+        +chart-settings
+        .empty(ng-show='paragraph.chartColumns.length > 0 && !paragraph.chartColumnsConfigured()') Cannot display chart. Please configure axis using #[b Chart settings]
+        .empty(ng-show='paragraph.chartColumns.length == 0') Cannot display chart. Result set must contain Java build-in type columns. Please change query and execute it again.
+        div(ng-show='paragraph.chartColumnsConfigured()')
+            div(ng-show='paragraph.timeLineSupported() || !paragraph.chartTimeLineEnabled()')
+                div(ng-repeat='chart in paragraph.charts')
+                    nvd3(options='chart.options' data='chart.data' api='chart.api')
+            .empty(ng-show='!paragraph.timeLineSupported() && paragraph.chartTimeLineEnabled()') Pie chart does not support 'TIME_LINE' column for X-axis. Please use another column for X-axis or switch to another chart.
+    .empty(ng-show='paragraph.scanExplain()')
+        .row
+            .col-xs-4.col-xs-offset-4
+                +result-toolbar
+        label.margin-top-dflt Charts do not support #[b Explain] and #[b Scan] query
+
+mixin paragraph-scan
+    panel-title {{ paragraph.name }}
+    panel-actions
+        query-actions-button(actions='$ctrl.scanActions' item='paragraph')
+    panel-content
+        .col-sm-12.sql-controls
+            .col-sm-3
+                +form-field__dropdown({
+                    label: 'Cache:',
+                    model: 'paragraph.cacheName',
+                    name: '"cache"',
+                    placeholder: 'Choose cache',
+                    options: 'caches'
+                })
+            .col-sm-3
+                +form-field__sensitive({
+                    label: 'Filter:',
+                    modelFilter: 'paragraph.filter',
+                    modelSensitive: 'paragraph.caseSensitive',
+                    name: '"filter"',
+                    placeholder: 'Enter filter'
+                })
+
+            .col-sm-3
+                +form-field__dropdown({
+                    label: 'Rows per page:',
+                    model: 'paragraph.pageSize',
+                    name: '"pageSize" + paragraph.id',
+                    options: 'pageSizesOptions',
+                    tip: 'Max number of rows to show in query result as one page',
+                    tipOpts: { placement: 'top' }
+                })
+
+        .col-sm-12.sql-controls
+            div
+                button.btn-ignite.btn-ignite--primary(ng-disabled='!scanAvailable(paragraph)' ng-click='scan(paragraph)')
+                    span.icon-left.fa.fa-fw.fa-play(ng-hide='paragraph.checkScanInProgress(false)')
+                    span.icon-left.fa.fa-fw.fa-refresh.fa-spin(ng-show='paragraph.checkScanInProgress(false)')
+                    | Scan
+
+                button.btn-ignite.btn-ignite--primary(ng-disabled='!scanAvailable(paragraph)' ng-click='scan(paragraph, true)')
+                    span.icon-left.fa.fa-fw.fa-play(ng-hide='paragraph.checkScanInProgress(true)')
+                    span.icon-left.fa.fa-fw.fa-refresh.fa-spin(ng-show='paragraph.checkScanInProgress(true)')
+                    | Scan on selected node
+
+                button.btn-ignite.btn-ignite--secondary(ng-if='paragraph.checkScanInProgress(false) || paragraph.checkScanInProgress(true)' ng-click='cancelQuery(paragraph)' data-placement='bottom' bs-tooltip='' data-title='{{"Cancel query execution"}}')
+                    | Cancel
+            div
+
+        .col-sm-12.sql-result(ng-if='paragraph.queryExecuted() && !paragraph.scanningInProgress' ng-switch='paragraph.resultType()')
+            .error(ng-switch-when='error') Error: {{paragraph.error.message}}
+            .empty(ng-switch-when='empty') Result set is empty. Duration: #[b {{paragraph.duration | duration}}]
+            .table(ng-switch-when='table')
+                +table-result-heading-scan
+                +table-result-body
+            .footer.clearfix()
+                .pull-left
+                    | Showing results for scan of #[b {{ paragraph.queryArgs.cacheName | defaultName }}]
+                    span(ng-if='paragraph.queryArgs.filter') &nbsp; with filter: #[b {{ paragraph.queryArgs.filter }}]
+                    span(ng-if='paragraph.queryArgs.localNid') &nbsp; on node: #[b {{ paragraph.queryArgs.localNid | limitTo:8 }}]
+
+                -var nextVisibleCondition = 'paragraph.resultType() != "error" && !paragraph.loading && paragraph.queryId && paragraph.nonRefresh() && (paragraph.table() || paragraph.chart() && !paragraph.scanExplain())'
+
+                .pull-right(ng-show=`${nextVisibleCondition}` ng-class='{disabled: paragraph.loading}' ng-click='!paragraph.loading && nextPage(paragraph)')
+                    i.fa.fa-chevron-circle-right
+                    a Next
+
+mixin paragraph-query
+    panel-title {{ paragraph.name }}
+    panel-actions
+        query-actions-button(actions='$ctrl.queryActions' item='paragraph')
+    panel-content
+        .col-sm-12
+            .col-xs-8.col-sm-9(style='border-right: 1px solid #eee')
+                .sql-editor(ignite-ace='{onLoad: aceInit(paragraph), theme: "chrome", mode: "sql", require: ["ace/ext/language_tools"],' +
+                'advanced: {enableSnippets: false, enableBasicAutocompletion: true, enableLiveAutocompletion: true}}'
+                ng-model='paragraph.query' on-selection-change='paragraph.partialQuery = $event')
+            .col-xs-4.col-sm-3
+                div(ng-show='caches.length > 0' style='padding: 5px 10px' st-table='displayedCaches' st-safe-src='caches')
+                    lable.labelField.labelFormField Caches:
+                    i.fa.fa-database.tipField(title='Click to show cache types metadata dialog' bs-popover data-template-url='{{ $ctrl.cacheMetadataTemplateUrl }}' data-placement='bottom-right' data-trigger='click' data-container='#{{ paragraph.id }}')
+                    .input-tip
+                        input.form-control(type='text' st-search='label' placeholder='Filter caches...')
+
+                    .queries-notebook-displayed-caches
+                        div(ng-repeat='cache in displayedCaches track by cache.value')
+                            +form-field__radio({
+                                label: '{{ cache.label }}',
+                                model: 'paragraph.cacheName',
+                                name: '"cache_" + [paragraph.id, $index].join("_")',
+                                value: 'cache.value'
+                            })
+
+                    .settings-row
+                        .row(ng-if='ddlAvailable(paragraph)')
+                            +form-field__checkbox({
+                                label: 'Use selected cache as default schema name',
+                                model: 'paragraph.useAsDefaultSchema',
+                                name: '"useAsDefaultSchema" + paragraph.id',
+                                tip: 'Use selected cache as default schema name.<br/>\
+                                    This will allow to execute query on specified cache without specify schema name.<br/>\
+                                    <b>NOTE:</b> In future version of Ignite this feature will be removed.',
+                                tipOpts: { placement: 'top' }
+                            })
+                .empty-caches(ng-show='displayedCaches.length == 0 && caches.length != 0')
+                    no-data
+                        label Wrong caches filter
+                .empty-caches(ng-show='caches.length == 0')
+                    no-data
+                        label No caches
+        .col-sm-12.sql-controls
+            div
+                +query-actions
+
+            +query-settings
+        .col-sm-12.sql-result(ng-if='paragraph.queryExecuted()' ng-switch='paragraph.resultType()')
+            .error(ng-switch-when='error')
+                label Error: {{paragraph.error.message}}
+                br
+                a(ng-show='paragraph.resultType() === "error"' ng-click='showStackTrace(paragraph)') Show more
+            .empty(ng-switch-when='empty') Result set is empty. Duration: #[b {{paragraph.duration | duration}}]
+            .table(ng-switch-when='table')
+                +table-result-heading-query
+                +table-result-body
+            .chart(ng-switch-when='chart')
+                +chart-result
+            .footer.clearfix(ng-show='paragraph.resultType() !== "error"')
+                a.pull-left(ng-click='showResultQuery(paragraph)') Show query
+
+                -var nextVisibleCondition = 'paragraph.resultType() !== "error" && !paragraph.loading && paragraph.queryId && paragraph.nonRefresh() && (paragraph.table() || paragraph.chart() && !paragraph.scanExplain())'
+
+                .pull-right(ng-show=`${nextVisibleCondition}` ng-class='{disabled: paragraph.loading}' ng-click='!paragraph.loading && nextPage(paragraph)')
+                    i.fa.fa-chevron-circle-right
+                    a Next
+
+div(ng-if='notebook')
+    .notebooks-top
+        h1(ng-hide='notebook.edit')
+            label {{notebook.name}}
+            .btn-group(ng-if='!demo')
+                +btn-toolbar('fa-pencil', 'notebook.edit = true;notebook.editName = notebook.name', 'Rename notebook')
+
+        h1(ng-show='notebook.edit')
+            input.form-control(ng-model='notebook.editName' required ignite-on-enter='renameNotebook(notebook.editName)' ignite-on-escape='notebook.edit = false;')
+            i.btn.fa.fa-floppy-o(ng-show='notebook.editName' ng-click='renameNotebook(notebook.editName)' bs-tooltip data-title='Save notebook name' data-trigger='hover')
+
+        cluster-selector
+
+        .notebook-top-buttons
+            a.dropdown-toggle(style='margin-right: 20px' data-toggle='dropdown' bs-dropdown='scrollParagraphs' data-placement='bottom-left') Scroll to query
+                span.caret
+            button.btn-ignite.btn-ignite--primary(ng-click='addQuery()' ignite-on-click-focus=focusId)
+                svg.icon-left(ignite-icon='plus')
+                | Add query
+
+            button.btn-ignite.btn-ignite--primary(ng-click='addScan()' ignite-on-click-focus=focusId)
+                svg.icon-left(ignite-icon='plus')
+                | Add scan
+
+div
+    breadcrumbs
+        a.link-success(ui-sref='base.sql.tabs') Notebooks
+        span(ui-sref='.' ui-sref-active) Notebook '{{notebook.name}}'
+
+    -var example = `CREATE TABLE Person(ID INTEGER PRIMARY KEY, NAME VARCHAR(100));\nINSERT INTO Person(ID, NAME) VALUES (1, 'Ed'), (2, 'Ann'), (3, 'Emma');\nSELECT * FROM Person;`;
+
+    ignite-information(
+        data-title='With query notebook you can'
+        style='margin-bottom: 30px'
+        ng-init=`example = "${example}"`
+    )
+        ul
+            li Create any number of queries
+            li Execute and explain SQL queries
+            li Execute scan queries
+            li View data in tabular form and as charts
+            li
+                a(href='https://apacheignite-sql.readme.io/docs/sql-reference-overview' target='_blank') More info
+        .example
+            .group
+                .group-legend
+                    label Examples:
+                .group-content
+                    .sql-editor(ignite-ace='{\
+                        onLoad: aceInit({}),\
+                        theme: "chrome",\
+                        mode: "sql",\
+                        require: ["ace/ext/language_tools"],\
+                        showGutter: false,\
+                        advanced: {\
+                           enableSnippets: false,\
+                           enableBasicAutocompletion: true,\
+                           enableLiveAutocompletion: true\
+                        }}'
+                        ng-model='example'
+                        readonly='true'
+                    )
+
+    .notebook-failed--block(ng-if='notebookLoadFailed')
+        no-data
+            h2 Failed to load notebook
+            label.col-sm-12 Notebook not accessible any more. Go back to notebooks list.
+
+    div(ng-if='notebook' ignite-loading='sqlLoading' ignite-loading-text='{{ loadingText }}' ignite-loading-position='top')
+        .docs-body.paragraphs
+            .panel-group
+                .panel-paragraph(ng-repeat='paragraph in notebook.paragraphs' id='{{paragraph.id}}' ng-form='form_{{paragraph.id}}')
+                    panel-collapsible(
+                        ng-if='paragraph.qryType === "scan"'
+                        opened='$ctrl.isParagraphOpened($index)'
+                        on-close='$ctrl.onParagraphClose($index)'
+                        on-open='$ctrl.onParagraphOpen($index)'
+                    )
+                        +paragraph-scan
+                    panel-collapsible(
+                        ng-if='paragraph.qryType === "query"'
+                        opened='$ctrl.isParagraphOpened($index)'
+                        on-close='$ctrl.onParagraphClose($index)'
+                        on-open='$ctrl.onParagraphOpen($index)'
+                    )
+                        +paragraph-query
diff --git a/modules/frontend/app/components/page-queries/components/queries-notebooks-list/controller.js b/modules/frontend/app/components/page-queries/components/queries-notebooks-list/controller.js
new file mode 100644
index 0000000..a10dd1f
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/components/queries-notebooks-list/controller.js
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class NotebooksListCtrl {
+    static $inject = ['IgniteNotebook', 'IgniteMessages', 'IgniteLoading', 'IgniteInput', '$scope', '$modal'];
+
+    constructor(IgniteNotebook, IgniteMessages, IgniteLoading, IgniteInput, $scope, $modal) {
+        Object.assign(this, { IgniteNotebook, IgniteMessages, IgniteLoading, IgniteInput, $scope, $modal });
+
+        this.notebooks = [];
+
+        this.rowsToShow = 8;
+
+        const notebookNameTemplate = `<div class="ui-grid-cell-contents notebook-name"><a ui-sref="base.sql.notebook({ noteId: row.entity._id })">{{ row.entity.name }}</a></div>`;
+        const sqlQueryTemplate = `<div class="ui-grid-cell-contents">{{row.entity.sqlQueriesParagraphsLength}}</div>`;
+        const scanQueryTemplate = `<div class="ui-grid-cell-contents">{{row.entity.scanQueriesPsaragraphsLength}}</div>`;
+
+        this.categories = [
+            { name: 'Name', visible: true, enableHiding: false },
+            { name: 'SQL Queries', visible: true, enableHiding: false },
+            { name: 'Scan Queries', visible: true, enableHiding: false }
+        ];
+
+        this.columnDefs = [
+            { name: 'name', displayName: 'Notebook name', categoryDisplayName: 'Name', field: 'name', cellTemplate: notebookNameTemplate, filter: { placeholder: 'Filter by Name...' } },
+            { name: 'sqlQueryNum', displayName: 'SQL Queries', categoryDisplayName: 'SQL Queries', field: 'sqlQueriesParagraphsLength', cellTemplate: sqlQueryTemplate, enableSorting: true, type: 'number', minWidth: 150, width: '10%', enableFiltering: false },
+            { name: 'scanQueryNum', displayName: 'Scan Queries', categoryDisplayName: 'Scan Queries', field: 'scanQueriesParagraphsLength', cellTemplate: scanQueryTemplate, enableSorting: true, type: 'number', minWidth: 150, width: '10%', enableFiltering: false }
+        ];
+
+        this.actionOptions = [
+            {
+                action: 'Clone',
+                click: this.cloneNotebook.bind(this),
+                available: true
+            },
+            {
+                action: 'Rename',
+                click: this.renameNotebok.bind(this),
+                available: true
+            },
+            {
+                action: 'Delete',
+                click: this.deleteNotebooks.bind(this),
+                available: true
+            }
+        ];
+    }
+
+    $onInit() {
+        this._loadAllNotebooks();
+    }
+
+    async _loadAllNotebooks() {
+        try {
+            this.IgniteLoading.start('notebooksLoading');
+
+            const data = await this.IgniteNotebook.read();
+
+            this.notebooks = this._preprocessNotebooksList(data);
+        }
+        catch (err) {
+            this.IgniteMessages.showError(err);
+        }
+        finally {
+            this.$scope.$applyAsync();
+
+            this.IgniteLoading.finish('notebooksLoading');
+        }
+    }
+
+    _preprocessNotebooksList(notebooks = []) {
+        return notebooks.map((notebook) => {
+            notebook.sqlQueriesParagraphsLength = this._countParagraphs(notebook, 'query');
+            notebook.scanQueriesPsaragraphsLength = this._countParagraphs(notebook, 'scan');
+
+            return notebook;
+        });
+    }
+
+    _countParagraphs(notebook, queryType = 'query') {
+        return notebook.paragraphs.filter((paragraph) => paragraph.qryType === queryType).length || 0;
+    }
+
+    onSelectionChanged() {
+        this._checkActionsAllow();
+    }
+
+    _checkActionsAllow() {
+        // Dissallow clone and rename if more then one item is selectted.
+        const oneItemIsSelected  = this.gridApi.selection.legacyGetSelectedRows().length === 1;
+        this.actionOptions[0].available = oneItemIsSelected;
+        this.actionOptions[1].available = oneItemIsSelected;
+    }
+
+    async createNotebook() {
+        try {
+            const newNotebookName = await this.IgniteInput.input('New query notebook', 'Notebook name');
+
+            this.IgniteLoading.start('notebooksLoading');
+
+            await this.IgniteNotebook.create(newNotebookName);
+
+            this.IgniteLoading.finish('notebooksLoading');
+
+            this._loadAllNotebooks();
+        }
+        catch (err) {
+            this.IgniteMessages.showError(err);
+        }
+        finally {
+            this.IgniteLoading.finish('notebooksLoading');
+
+            if (this.createNotebookModal)
+                this.createNotebookModal.$promise.then(this.createNotebookModal.hide);
+        }
+    }
+
+    async renameNotebok() {
+        try {
+            const currentNotebook = this.gridApi.selection.legacyGetSelectedRows()[0];
+            const newNotebookName = await this.IgniteInput.input('Rename notebook', 'Notebook name', currentNotebook.name);
+
+            if (this.getNotebooksNames().find((name) => newNotebookName === name))
+                throw Error(`Notebook with name "${newNotebookName}" already exists!`);
+
+            this.IgniteLoading.start('notebooksLoading');
+
+            await this.IgniteNotebook.save(Object.assign(currentNotebook, {name: newNotebookName}));
+        }
+        catch (err) {
+            this.IgniteMessages.showError(err);
+        }
+        finally {
+            this.IgniteLoading.finish('notebooksLoading');
+
+            this._loadAllNotebooks();
+        }
+    }
+
+    async cloneNotebook() {
+        try {
+            const clonedNotebook = Object.assign({}, this.gridApi.selection.legacyGetSelectedRows()[0]);
+            const newNotebookName = await this.IgniteInput.clone(clonedNotebook.name, this.getNotebooksNames());
+
+            this.IgniteLoading.start('notebooksLoading');
+
+            await this.IgniteNotebook.clone(newNotebookName, clonedNotebook);
+
+            this.IgniteLoading.finish('notebooksLoading');
+
+            this._loadAllNotebooks();
+        }
+        catch (err) {
+            this.IgniteMessages.showError(err);
+        }
+        finally {
+            this.IgniteLoading.finish('notebooksLoading');
+
+            if (this.createNotebookModal)
+                this.createNotebookModal.$promise.then(this.createNotebookModal.hide);
+        }
+    }
+
+    getNotebooksNames() {
+        return this.notebooks.map((notebook) => notebook.name);
+    }
+
+    async deleteNotebooks() {
+        try {
+            this.IgniteLoading.start('notebooksLoading');
+
+            await this.IgniteNotebook.removeBatch(this.gridApi.selection.legacyGetSelectedRows());
+
+            this.IgniteLoading.finish('notebooksLoading');
+
+            this._loadAllNotebooks();
+        }
+        catch (err) {
+            this.IgniteMessages.showError(err);
+
+            this.IgniteLoading.finish('notebooksLoading');
+        }
+    }
+
+    _adjustHeight(rows) {
+        // Add header height.
+        const height = Math.min(rows, this.rowsToShow) * 48 + 78;
+
+        this.gridApi.grid.element.css('height', height + 'px');
+
+        this.gridApi.core.handleWindowResize();
+    }
+}
diff --git a/modules/frontend/app/components/page-queries/components/queries-notebooks-list/index.js b/modules/frontend/app/components/page-queries/components/queries-notebooks-list/index.js
new file mode 100644
index 0000000..a50198e
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/components/queries-notebooks-list/index.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import templateUrl from './template.tpl.pug';
+import {NotebooksListCtrl} from './controller';
+import './style.scss';
+
+export default angular.module('ignite-console.sql.notebooks-list', [])
+    .component('queriesNotebooksList', {
+        controller: NotebooksListCtrl,
+        templateUrl,
+        transclude: true
+    });
diff --git a/modules/frontend/app/components/page-queries/components/queries-notebooks-list/style.scss b/modules/frontend/app/components/page-queries/components/queries-notebooks-list/style.scss
new file mode 100644
index 0000000..10fd40a
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/components/queries-notebooks-list/style.scss
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#createNotebookBtn {
+  [ignite-icon] {
+    height: 12px;
+  }
+}
+
+.queries-notebooks-list {
+  grid-no-data {
+    background: white;
+  }
+
+  .ui-grid-render-container-left:before {
+    display: none;
+  }
+
+  .notebook-name {
+    a {
+      color: #0067b9;
+    }
+  }
+}
+
diff --git a/modules/frontend/app/components/page-queries/components/queries-notebooks-list/template.tpl.pug b/modules/frontend/app/components/page-queries/components/queries-notebooks-list/template.tpl.pug
new file mode 100644
index 0000000..a637556
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/components/queries-notebooks-list/template.tpl.pug
@@ -0,0 +1,56 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+page-queries-slot(slot-name="'queriesTitle'")
+    h1 Queries
+
+page-queries-slot(slot-name="'queriesButtons'" ng-if="!$root.IgniteDemoMode")
+    button#createNotebookBtn.btn-ignite.btn-ignite--primary(ng-click='$ctrl.createNotebook()')
+        svg.icon-left(ignite-icon='plus')
+        | Create Notebook
+
+
+.queries-notebooks-list
+    .panel--ignite
+        header.header-with-selector
+            div
+                span Notebooks
+
+            div(ng-if="!$root.IgniteDemoMode")
+                +ignite-form-field-bsdropdown({
+                    label: 'Actions',
+                    model: '$ctrl.action',
+                    name: 'action',
+                    disabled: '$ctrl.gridApi.selection.legacyGetSelectedRows().length === 0',
+                    options: '$ctrl.actionOptions'
+                })
+
+        .panel-collapse(ignite-loading='notebooksLoading' ignite-loading-text='Loading notebooks...')
+            ignite-grid-table(
+                items='$ctrl.notebooks'
+                column-defs='$ctrl.columnDefs'
+                grid-api='$ctrl.gridApi'
+                grid-thin='true'
+                on-selection-change='$ctrl.onSelectionChanged()'
+            )
+
+            grid-no-data(grid-api='$ctrl.gridApi')
+                | You have no notebooks.
+                a.link-success(ng-click='$ctrl.createNotebook()') Create one?
+                grid-no-data-filtered
+                    | Nothing to display. Check your filters.
diff --git a/modules/frontend/app/components/page-queries/index.ts b/modules/frontend/app/components/page-queries/index.ts
new file mode 100644
index 0000000..34fa388
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/index.ts
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './style.scss';
+
+import angular from 'angular';
+
+import queriesNotebooksList from './components/queries-notebooks-list';
+import queriesNotebook from './components/queries-notebook';
+import pageQueriesCmp from './component';
+import {default as ActivitiesData} from 'app/core/activities/Activities.data';
+import Notebook from './notebook.service';
+import {AppStore, navigationMenuItem} from '../../store';
+
+/**
+ * @param {import('@uirouter/angularjs').UIRouter} $uiRouter
+ * @param {ActivitiesData} ActivitiesData
+ */
+function registerActivitiesHook($uiRouter, ActivitiesData) {
+    $uiRouter.transitionService.onSuccess({to: 'base.sql.**'}, (transition) => {
+        ActivitiesData.post({group: 'sql', action: transition.targetState().name()});
+    });
+}
+
+registerActivitiesHook.$inject = ['$uiRouter', 'IgniteActivitiesData'];
+
+export default angular.module('ignite-console.sql', [
+    'ui.router',
+    queriesNotebooksList.name,
+    queriesNotebook.name
+])
+    .run(['Store', (store: AppStore) => {
+        store.dispatch(navigationMenuItem({
+            activeSref: 'base.sql.**',
+            icon: 'sql',
+            label: 'Queries',
+            order: 2,
+            sref: 'base.sql.tabs.notebooks-list'
+        }));
+    }])
+    .component('pageQueries', pageQueriesCmp)
+    .component('pageQueriesSlot', {
+        require: {
+            pageQueries: '^pageQueries'
+        },
+        bindings: {
+            slotName: '<'
+        },
+        controller: class {
+            static $inject = ['$transclude', '$timeout'];
+
+            constructor($transclude, $timeout) {
+                this.$transclude = $transclude;
+                this.$timeout = $timeout;
+            }
+
+            $postLink() {
+                this.$transclude((clone) => {
+                    this.pageQueries[this.slotName].empty();
+                    clone.appendTo(this.pageQueries[this.slotName]);
+                });
+            }
+        },
+        transclude: true
+    })
+    .service('IgniteNotebook', Notebook)
+    .config(['$stateProvider', ($stateProvider) => {
+        // set up the states
+        $stateProvider
+            .state('base.sql', {
+                abstract: true
+            })
+            .state('base.sql.tabs', {
+                url: '/queries',
+                component: 'pageQueries',
+                redirectTo: 'base.sql.tabs.notebooks-list',
+                permission: 'query'
+            })
+            .state('base.sql.tabs.notebooks-list', {
+                url: '/notebooks',
+                component: 'queriesNotebooksList',
+                permission: 'query',
+                tfMetaTags: {
+                    title: 'Notebooks'
+                }
+            })
+            .state('base.sql.notebook', {
+                url: '/notebook/{noteId}',
+                component: 'queriesNotebook',
+                permission: 'query',
+                tfMetaTags: {
+                    title: 'Query notebook'
+                }
+            });
+    }])
+    .run(registerActivitiesHook);
diff --git a/modules/frontend/app/components/page-queries/notebook.data.js b/modules/frontend/app/components/page-queries/notebook.data.js
new file mode 100644
index 0000000..d9035c2
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/notebook.data.js
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const DEMO_NOTEBOOK = {
+    name: 'SQL demo',
+    _id: 'demo',
+    paragraphs: [
+        {
+            name: 'Query with refresh rate',
+            qryType: 'query',
+            pageSize: 100,
+            limit: 0,
+            query: [
+                'SELECT count(*)',
+                'FROM "CarCache".Car'
+            ].join('\n'),
+            result: 'bar',
+            timeLineSpan: '1',
+            rate: {
+                value: 3,
+                unit: 1000,
+                installed: true
+            }
+        },
+        {
+            name: 'Simple query',
+            qryType: 'query',
+            pageSize: 100,
+            limit: 0,
+            query: 'SELECT * FROM "CarCache".Car',
+            result: 'table',
+            timeLineSpan: '1',
+            rate: {
+                value: 30,
+                unit: 1000,
+                installed: false
+            }
+        },
+        {
+            name: 'Query with aggregates',
+            qryType: 'query',
+            pageSize: 100,
+            limit: 0,
+            query: [
+                'SELECT p.name, count(*) AS cnt',
+                'FROM "ParkingCache".Parking p',
+                'INNER JOIN "CarCache".Car c',
+                '  ON (p.id) = (c.parkingId)',
+                'GROUP BY P.NAME'
+            ].join('\n'),
+            result: 'table',
+            timeLineSpan: '1',
+            rate: {
+                value: 30,
+                unit: 1000,
+                installed: false
+            }
+        }
+    ],
+    expandedParagraphs: [0, 1, 2]
+};
+
+export default class NotebookData {
+    static $inject = ['$rootScope', '$http', '$q'];
+
+    /**
+     * @param {ng.IRootScopeService} $root 
+     * @param {ng.IHttpService} $http 
+     * @param {ng.IQService} $q    
+     */
+    constructor($root, $http, $q) {
+        this.demo = $root.IgniteDemoMode;
+
+        this.initLatch = null;
+        this.notebooks = null;
+
+        this.$http = $http;
+        this.$q = $q;
+    }
+
+    load() {
+        if (this.demo) {
+            if (this.initLatch)
+                return this.initLatch;
+
+            return this.initLatch = this.$q.when(this.notebooks = [DEMO_NOTEBOOK]);
+        }
+
+        return this.initLatch = this.$http.get('/api/v1/notebooks')
+            .then(({data}) => this.notebooks = data)
+            .catch(({data}) => Promise.reject(data));
+    }
+
+    read() {
+        if (this.initLatch)
+            return this.initLatch;
+
+        return this.load();
+    }
+
+    find(_id) {
+        return this.read()
+            .then(() => {
+                const notebook = this.demo ? this.notebooks[0] : _.find(this.notebooks, {_id});
+
+                if (_.isNil(notebook))
+                    return this.$q.reject('Failed to load notebook.');
+
+                return notebook;
+            });
+    }
+
+    findIndex(notebook) {
+        return this.read()
+            .then(() => _.findIndex(this.notebooks, {_id: notebook._id}));
+    }
+
+    save(notebook) {
+        if (this.demo)
+            return this.$q.when(DEMO_NOTEBOOK);
+
+        return this.$http.post('/api/v1/notebooks/save', notebook)
+            .then(({data}) => {
+                const idx = _.findIndex(this.notebooks, {_id: data._id});
+
+                if (idx >= 0)
+                    this.notebooks[idx] = data;
+                else
+                    this.notebooks.push(data);
+
+                return data;
+            })
+            .catch(({data}) => Promise.reject(data));
+    }
+
+    remove(notebook) {
+        if (this.demo)
+            return this.$q.reject(`Removing "${notebook.name}" notebook is not supported.`);
+
+        const key = {_id: notebook._id};
+
+        return this.$http.post('/api/v1/notebooks/remove', key)
+            .then(() => {
+                const idx = _.findIndex(this.notebooks, key);
+
+                if (idx >= 0) {
+                    this.notebooks.splice(idx, 1);
+
+                    if (idx < this.notebooks.length)
+                        return this.notebooks[idx];
+                }
+
+                if (this.notebooks.length > 0)
+                    return this.notebooks[this.notebooks.length - 1];
+
+                return null;
+            })
+            .catch(({data}) => Promise.reject(data));
+    }
+}
diff --git a/modules/frontend/app/components/page-queries/notebook.service.js b/modules/frontend/app/components/page-queries/notebook.service.js
new file mode 100644
index 0000000..2a60cdc
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/notebook.service.js
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class Notebook {
+    static $inject = ['$state', 'IgniteConfirm', 'IgniteMessages', 'IgniteNotebookData'];
+
+    /**
+     * @param {import('@uirouter/angularjs').StateService} $state
+     * @param {ReturnType<typeof import('app/services/Confirm.service').default>} confirmModal
+     * @param {ReturnType<typeof import('app/services/Messages.service').default>} Messages
+     * @param {import('./notebook.data').default} NotebookData
+     */
+    constructor($state, confirmModal, Messages, NotebookData) {
+        this.$state = $state;
+        this.confirmModal = confirmModal;
+        this.Messages = Messages;
+        this.NotebookData = NotebookData;
+    }
+
+    read() {
+        return this.NotebookData.read();
+    }
+
+    create(name) {
+        return this.NotebookData.save({name});
+    }
+
+    save(notebook) {
+        return this.NotebookData.save(notebook);
+    }
+
+    async clone(newNotebookName, clonedNotebook) {
+        const newNotebook = await this.create(newNotebookName);
+        Object.assign(clonedNotebook, {name: newNotebook.name, _id: newNotebook._id });
+
+        return this.save(clonedNotebook);
+    }
+
+    find(_id) {
+        return this.NotebookData.find(_id);
+    }
+
+    _openNotebook(idx) {
+        return this.NotebookData.read()
+            .then((notebooks) => {
+                const nextNotebook = notebooks.length > idx ? notebooks[idx] : _.last(notebooks);
+
+                if (nextNotebook)
+                    this.$state.go('base.sql.tabs.notebook', {noteId: nextNotebook._id});
+                else
+                    this.$state.go('base.sql.tabs.notebooks-list');
+            });
+    }
+
+    remove(notebook) {
+        return this.confirmModal.confirm(`Are you sure you want to remove notebook: "${notebook.name}"?`)
+            .then(() => this.NotebookData.findIndex(notebook))
+            .then((idx) => {
+                return this.NotebookData.remove(notebook)
+                    .then(() => {
+                        if (this.$state.includes('base.sql.tabs.notebook') && this.$state.params.noteId === notebook._id)
+                            return this._openNotebook(idx);
+                    })
+                    .catch(this.Messages.showError);
+            });
+    }
+
+    removeBatch(notebooks) {
+        return this.confirmModal.confirm(`Are you sure you want to remove ${notebooks.length} selected notebooks?`)
+            .then(() => {
+                const deleteNotebooksPromises = notebooks.map((notebook) => this.NotebookData.remove(notebook));
+
+                return Promise.all(deleteNotebooksPromises);
+            })
+            .catch(this.Messages.showError);
+    }
+}
diff --git a/modules/frontend/app/components/page-queries/style.scss b/modules/frontend/app/components/page-queries/style.scss
new file mode 100644
index 0000000..818fb1c
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/style.scss
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+button.select-toggle.btn-chart-column-agg-fx::after {
+	right: 0;
+}
+
+.sql-controls {
+	flex-wrap: wrap;
+}
diff --git a/modules/frontend/app/components/page-queries/template.tpl.pug b/modules/frontend/app/components/page-queries/template.tpl.pug
new file mode 100644
index 0000000..50d2135
--- /dev/null
+++ b/modules/frontend/app/components/page-queries/template.tpl.pug
@@ -0,0 +1,30 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+header.header-with-selector
+    div
+        .queries-title
+        cluster-selector
+    .queries-buttons
+
+div
+    ul.tabs.tabs--blue
+        li(role='presentation' ui-sref-active='active')
+            a(ui-sref='base.sql.tabs.notebooks-list')
+                span Notebooks
+                span.badge.badge--blue {{ $ctrl.notebooks.length }}
+
+    ui-view
diff --git a/modules/frontend/app/components/page-signin/component.ts b/modules/frontend/app/components/page-signin/component.ts
new file mode 100644
index 0000000..6c5f461
--- /dev/null
+++ b/modules/frontend/app/components/page-signin/component.ts
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+import './style.scss';
+
+/** @type {ng.IComponentOptions} */
+export default {
+    controller,
+    template,
+    bindings: {
+        activationToken: '@?'
+    }
+};
diff --git a/modules/frontend/app/components/page-signin/controller.ts b/modules/frontend/app/components/page-signin/controller.ts
new file mode 100644
index 0000000..d242e2a
--- /dev/null
+++ b/modules/frontend/app/components/page-signin/controller.ts
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import AuthService from 'app/modules/user/Auth.service';
+
+import {PageSigninStateParams} from './run';
+
+interface ISiginData {
+    email: string,
+    password: string
+}
+
+interface ISigninFormController extends ng.IFormController {
+    email: ng.INgModelController,
+    password: ng.INgModelController
+}
+
+export default class PageSignIn implements ng.IPostLink {
+    activationToken?: PageSigninStateParams['activationToken'];
+
+    data: ISiginData = {
+        email: null,
+        password: null
+    };
+
+    form: ISigninFormController;
+
+    serverError: string = null;
+
+    isLoading = false;
+
+    static $inject = ['Auth', 'IgniteMessages', 'IgniteFormUtils', '$element'];
+
+    constructor(private Auth: AuthService, private IgniteMessages, private IgniteFormUtils, private el: JQLite) {}
+
+    canSubmitForm(form: ISigninFormController) {
+        return form.$error.server ? true : !form.$invalid;
+    }
+
+    $postLink() {
+        this.el.addClass('public-page');
+        this.form.email.$validators.server = () => !this.serverError;
+        this.form.password.$validators.server = () => !this.serverError;
+    }
+
+    setServerError(error: string) {
+        this.serverError = error;
+        this.form.email.$validate();
+        this.form.password.$validate();
+    }
+
+    signin() {
+        this.isLoading = true;
+
+        this.IgniteFormUtils.triggerValidation(this.form);
+
+        this.setServerError(null);
+
+        if (!this.canSubmitForm(this.form)) {
+            this.isLoading = false;
+            return;
+        }
+
+        return this.Auth.signin(this.data.email, this.data.password, this.activationToken).catch((res) => {
+            this.IgniteMessages.showError(null, res.data.errorMessage ? res.data.errorMessage : res.data);
+
+            this.setServerError(res.data);
+
+            this.IgniteFormUtils.triggerValidation(this.form);
+
+            this.isLoading = false;
+        });
+    }
+}
diff --git a/modules/frontend/app/components/page-signin/index.ts b/modules/frontend/app/components/page-signin/index.ts
new file mode 100644
index 0000000..ee027a6
--- /dev/null
+++ b/modules/frontend/app/components/page-signin/index.ts
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+import {registerState} from './run';
+
+export default angular
+    .module('ignite-console.page-signin', [
+        'ui.router',
+        'ignite-console.user'
+    ])
+    .component('pageSignin', component)
+    .run(registerState);
diff --git a/modules/frontend/app/components/page-signin/run.ts b/modules/frontend/app/components/page-signin/run.ts
new file mode 100644
index 0000000..62f786e
--- /dev/null
+++ b/modules/frontend/app/components/page-signin/run.ts
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import publicTemplate from '../../../views/public.pug';
+import {StateParams, UIRouter} from '@uirouter/angularjs';
+import {IIgniteNg1StateDeclaration} from 'app/types';
+
+export type PageSigninStateParams = StateParams & {activationToken?: string};
+
+export function registerState($uiRouter: UIRouter) {
+    const state: IIgniteNg1StateDeclaration = {
+        url: '/signin?{activationToken:string}',
+        name: 'signin',
+        views: {
+            '': {
+                template: publicTemplate
+            },
+            'page@signin': {
+                component: 'pageSignin'
+            }
+        },
+        unsaved: true,
+        redirectTo: (trans) => {
+            const skipStates = new Set(['signup', 'forgotPassword', 'landing']);
+
+            if (skipStates.has(trans.from().name))
+                return;
+
+            return trans.injector().get('User').read()
+                .then(() => {
+                    try {
+                        const {name, params} = JSON.parse(localStorage.getItem('lastStateChangeSuccess'));
+
+                        const restored = trans.router.stateService.target(name, params);
+
+                        return restored.valid() ? restored : 'default-state';
+                    } catch (ignored) {
+                        return 'default-state';
+                    }
+                })
+                .catch(() => true);
+        },
+        tfMetaTags: {
+            title: 'Sign In'
+        },
+        resolve: {
+            activationToken() {
+                return $uiRouter.stateService.transition.params<PageSigninStateParams>().activationToken;
+            }
+        }
+    };
+
+    $uiRouter.stateRegistry.register(state);
+}
+
+registerState.$inject = ['$uiRouter'];
diff --git a/modules/frontend/app/components/page-signin/style.scss b/modules/frontend/app/components/page-signin/style.scss
new file mode 100644
index 0000000..729d439
--- /dev/null
+++ b/modules/frontend/app/components/page-signin/style.scss
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+page-signin {
+    display: flex;
+    flex-direction: column;
+
+    .form-field {
+        margin: 10px 0;
+    }
+
+    .form-footer {
+        padding: 15px 0;
+        text-align: right;
+        display: flex;
+        align-items: center;
+
+        .btn-ignite {
+            margin-left: auto;
+        }
+    }
+
+    .page-signin__no-account-message {
+        text-align: center;
+        margin: 20px 0;
+    } 
+}
diff --git a/modules/frontend/app/components/page-signin/template.pug b/modules/frontend/app/components/page-signin/template.pug
new file mode 100644
index 0000000..ad110c8
--- /dev/null
+++ b/modules/frontend/app/components/page-signin/template.pug
@@ -0,0 +1,55 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+global-progress-line(is-loading='$ctrl.isLoading')
+
+h3.public-page__title Sign In
+p(ng-if='$ctrl.activationToken')
+    | Please sign in to confirm your registration
+form(name='$ctrl.form' novalidate ng-submit='$ctrl.signin()')
+    +form-field__email({
+        label: 'Email:',
+        model: '$ctrl.data.email',
+        name: '"email"',
+        placeholder: 'Input email',
+        required: true
+    })(
+        ng-model-options='{allowInvalid: true}'
+        autocomplete='email'
+        ignite-auto-focus
+    )
+        +form-field__error({error: 'server', message: `{{$ctrl.serverError}}`})
+    +form-field__password({
+        label: 'Password:',
+        model: '$ctrl.data.password',
+        name: '"password"',
+        placeholder: 'Input password',
+        required: true
+    })(
+        ng-model-options='{allowInvalid: true}'
+        autocomplete='current-password'
+    )
+        +form-field__error({error: 'server', message: `{{$ctrl.serverError}}`})
+    footer.form-footer
+        a(ui-sref='forgotPassword({email: $ctrl.data.email})') Forgot password?
+        button.btn-ignite.btn-ignite--primary(
+            type='submit'
+            ng-disabled='$ctrl.isLoading'
+        ) {{ ::$ctrl.activationToken ? "Activate" : "Sign In" }}
+footer.page-signin__no-account-message
+    | Don't have an account? #[a(ui-sref='signup') Get started]
diff --git a/modules/frontend/app/components/page-signup-confirmation/component.ts b/modules/frontend/app/components/page-signup-confirmation/component.ts
new file mode 100644
index 0000000..3a1cc81
--- /dev/null
+++ b/modules/frontend/app/components/page-signup-confirmation/component.ts
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import controller from './controller';
+import templateUrl from './template.tpl.pug';
+import './style.scss';
+
+export const component: ng.IComponentOptions = {
+    controller,
+    templateUrl,
+    bindings: {
+        email: '@'
+    }
+};
diff --git a/modules/frontend/app/components/page-signup-confirmation/controller.ts b/modules/frontend/app/components/page-signup-confirmation/controller.ts
new file mode 100644
index 0000000..e5f5c89
--- /dev/null
+++ b/modules/frontend/app/components/page-signup-confirmation/controller.ts
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {default as Auth} from '../../modules/user/Auth.service';
+import {default as MessagesFactory} from '../../services/Messages.service';
+
+export default class PageSignupConfirmation {
+    email: string;
+
+    static $inject = ['Auth', 'IgniteMessages', '$element'];
+
+    constructor(private auth: Auth, private messages: ReturnType<typeof MessagesFactory>, private el: JQLite) {
+    }
+
+    $postLink() {
+        this.el.addClass('public-page');
+    }
+
+    async resendConfirmation() {
+        try {
+            await this.auth.resendSignupConfirmation(this.email);
+            this.messages.showInfo('Signup confirmation sent, check your email');
+        }
+        catch (e) {
+            this.messages.showError(e);
+        }
+    }
+}
diff --git a/modules/frontend/app/components/page-signup-confirmation/index.ts b/modules/frontend/app/components/page-signup-confirmation/index.ts
new file mode 100644
index 0000000..8df7532
--- /dev/null
+++ b/modules/frontend/app/components/page-signup-confirmation/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {component} from './component';
+import {state} from './state';
+
+export default angular.module('ignite-console.page-signup-confirmation', [])
+    .run(state)
+    .component('pageSignupConfirmation', component);
diff --git a/modules/frontend/app/components/page-signup-confirmation/state.ts b/modules/frontend/app/components/page-signup-confirmation/state.ts
new file mode 100644
index 0000000..31ad0d8
--- /dev/null
+++ b/modules/frontend/app/components/page-signup-confirmation/state.ts
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {StateParams, UIRouter} from '@uirouter/angularjs';
+import {IIgniteNg1StateDeclaration} from '../../types';
+import publicTemplate from '../../../views/public.pug';
+
+export type PageSignupConfirmationStateParams = StateParams & {email: string};
+
+state.$inject = ['$uiRouter'];
+
+export function state(router: UIRouter) {
+    router.stateRegistry.register({
+        name: 'signup-confirmation',
+        url: '/signup-confirmation?{email:string}',
+        views: {
+            '': {
+                template: publicTemplate
+            },
+            'page@signup-confirmation': {
+                component: 'pageSignupConfirmation'
+            }
+        },
+        unsaved: true,
+        tfMetaTags: {
+            title: 'Sign Up Confirmation'
+        },
+        resolve: {
+            email() {
+                return router.stateService.transition.params<PageSignupConfirmationStateParams>().email;
+            }
+        }
+    } as IIgniteNg1StateDeclaration);
+}
diff --git a/modules/frontend/app/components/page-signup-confirmation/style.scss b/modules/frontend/app/components/page-signup-confirmation/style.scss
new file mode 100644
index 0000000..d3c062b
--- /dev/null
+++ b/modules/frontend/app/components/page-signup-confirmation/style.scss
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+page-signup-confirmation {
+    display: flex;
+    flex-direction: column;
+    flex: 1 0 auto;
+}
diff --git a/modules/frontend/app/components/page-signup-confirmation/template.tpl.pug b/modules/frontend/app/components/page-signup-confirmation/template.tpl.pug
new file mode 100644
index 0000000..a1d183c
--- /dev/null
+++ b/modules/frontend/app/components/page-signup-confirmation/template.tpl.pug
@@ -0,0 +1,24 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+
+h3.public-page__title Confirm your email
+p
+    | Thanks For Signing Up!
+    br
+    | Please check your email and click link in the message we just sent to <b>{{::$ctrl.email}}</b>.
+    br
+    | If you don’t receive email try to <a ng-click='$ctrl.resendConfirmation()'>resend confirmation</a> once more.
diff --git a/modules/frontend/app/components/page-signup/component.js b/modules/frontend/app/components/page-signup/component.js
new file mode 100644
index 0000000..968ff39
--- /dev/null
+++ b/modules/frontend/app/components/page-signup/component.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+import './style.scss';
+
+/** @type {ng.IComponentOptions} */
+export default {
+    controller,
+    template
+};
diff --git a/modules/frontend/app/components/page-signup/controller.ts b/modules/frontend/app/components/page-signup/controller.ts
new file mode 100644
index 0000000..6e29869
--- /dev/null
+++ b/modules/frontend/app/components/page-signup/controller.ts
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Auth from '../../modules/user/Auth.service';
+import MessagesFactory from '../../services/Messages.service';
+import FormUtilsFactoryFactory from '../../services/FormUtils.service';
+import {ISignupData} from '../form-signup';
+import {eq, get, pipe} from 'lodash/fp';
+
+const EMAIL_NOT_CONFIRMED_ERROR_CODE = 10104;
+const isEmailConfirmationError = pipe(get('data.errorCode'), eq(EMAIL_NOT_CONFIRMED_ERROR_CODE));
+
+export default class PageSignup implements ng.IPostLink {
+    form: ng.IFormController;
+
+    data: ISignupData = {
+        email: null,
+        password: null,
+        firstName: null,
+        lastName: null,
+        company: null,
+        country: null
+    };
+
+    serverError: string | null = null;
+
+    isLoading = false;
+
+    static $inject = ['Auth', 'IgniteMessages', 'IgniteFormUtils', '$element'];
+
+    constructor(
+        private Auth: Auth,
+        private IgniteMessages: ReturnType<typeof MessagesFactory>,
+        private IgniteFormUtils: ReturnType<typeof FormUtilsFactoryFactory>,
+        private el: JQLite
+    ) {}
+
+    $postLink() {
+        this.el.addClass('public-page');
+    }
+
+    canSubmitForm(form: PageSignup['form']) {
+        return form.$error.server ? true : !form.$invalid;
+    }
+
+    setServerError(error: PageSignup['serverError']) {
+        this.serverError = error;
+    }
+
+    signup() {
+        this.isLoading = true;
+
+        this.IgniteFormUtils.triggerValidation(this.form);
+
+        this.setServerError(null);
+
+        if (!this.canSubmitForm(this.form)) {
+            this.isLoading = false;
+            return;
+        }
+
+
+        return this.Auth.signup(this.data)
+            .catch((res) => {
+                if (isEmailConfirmationError(res))
+                    return;
+
+                this.IgniteMessages.showError(null, res.data);
+                this.setServerError(res.data);
+            })
+            .finally(() => this.isLoading = false);
+    }
+}
diff --git a/modules/frontend/app/components/page-signup/index.js b/modules/frontend/app/components/page-signup/index.js
new file mode 100644
index 0000000..001d269
--- /dev/null
+++ b/modules/frontend/app/components/page-signup/index.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+import {registerState} from './run';
+
+export default angular
+    .module('ignite-console.page-signup', [
+        'ui.router',
+        'ignite-console.user',
+        'ignite-console.form-signup'
+    ])
+    .component('pageSignup', component)
+    .run(registerState);
diff --git a/modules/frontend/app/components/page-signup/run.js b/modules/frontend/app/components/page-signup/run.js
new file mode 100644
index 0000000..bd27638
--- /dev/null
+++ b/modules/frontend/app/components/page-signup/run.js
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import publicTemplate from '../../../views/public.pug';
+
+/**
+ * @param {import("@uirouter/angularjs").UIRouter} $uiRouter
+ */
+export function registerState($uiRouter) {
+    /** @type {import("app/types").IIgniteNg1StateDeclaration} */
+    const state = {
+        name: 'signup',
+        url: '/signup',
+        views: {
+            '': {
+                template: publicTemplate
+            },
+            'page@signup': {
+                component: 'pageSignup'
+            }
+        },
+        unsaved: true,
+        tfMetaTags: {
+            title: 'Sign Up'
+        }
+    };
+    $uiRouter.stateRegistry.register(state);
+}
+
+registerState.$inject = ['$uiRouter'];
diff --git a/modules/frontend/app/components/page-signup/style.scss b/modules/frontend/app/components/page-signup/style.scss
new file mode 100644
index 0000000..0930036
--- /dev/null
+++ b/modules/frontend/app/components/page-signup/style.scss
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+page-signup {
+    display: flex;
+    flex-direction: column;
+    flex: 1 0 auto;
+
+    form footer {
+        padding: 15px 0;
+        text-align: right;
+        display: flex;
+        align-items: center;
+
+        .btn-ignite {
+            margin-left: auto;
+        }
+    }
+
+    .page-signup__has-account-message {
+        text-align: center;
+        margin: 20px 0;
+    } 
+}
diff --git a/modules/frontend/app/components/page-signup/template.pug b/modules/frontend/app/components/page-signup/template.pug
new file mode 100644
index 0000000..e11c451
--- /dev/null
+++ b/modules/frontend/app/components/page-signup/template.pug
@@ -0,0 +1,32 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+global-progress-line(is-loading='$ctrl.isLoading')
+
+h3.public-page__title Don't Have An Account?
+form(name='$ctrl.form' novalidate ng-submit='$ctrl.signup()')
+    form-signup(
+        outer-form='$ctrl.form'
+        ng-model='$ctrl.data'
+        server-error='$ctrl.serverError'
+    )
+    footer.full-width.form-footer
+        button.btn-ignite.btn-ignite--primary(
+            type='submit'
+            ng-disabled='$ctrl.isLoading'
+        ) Sign Up
+footer.page-signup__has-account-message
+    | Already have an account? #[a(ui-sref='signin') Sign in here]
diff --git a/modules/frontend/app/components/panel-collapsible/component.js b/modules/frontend/app/components/panel-collapsible/component.js
new file mode 100644
index 0000000..f0cdc65
--- /dev/null
+++ b/modules/frontend/app/components/panel-collapsible/component.js
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+import './style.scss';
+
+export default {
+    template,
+    controller,
+    bindings: {
+        opened: '<?',
+        onOpen: '&?',
+        onClose: '&?',
+        title: '@?',
+        description: '@?',
+        disabled: '@?'
+    },
+    transclude: {
+        title: '?panelTitle',
+        description: '?panelDescription',
+        actions: '?panelActions',
+        content: 'panelContent'
+    }
+};
diff --git a/modules/frontend/app/components/panel-collapsible/controller.js b/modules/frontend/app/components/panel-collapsible/controller.js
new file mode 100644
index 0000000..65ec06a
--- /dev/null
+++ b/modules/frontend/app/components/panel-collapsible/controller.js
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class PanelCollapsible {
+    /** @type {Boolean} */
+    opened;
+    /** @type {ng.ICompiledExpression} */
+    onOpen;
+    /** @type {ng.ICompiledExpression} */
+    onClose;
+    /** @type {String} */
+    disabled;
+
+    static $inject = ['$transclude'];
+
+    /**
+     * @param {ng.ITranscludeFunction} $transclude
+     */
+    constructor($transclude) {
+        this.$transclude = $transclude;
+    }
+
+    toggle() {
+        if (this.opened)
+            this.close();
+        else
+            this.open();
+    }
+
+    open() {
+        if (this.disabled)
+            return;
+
+        this.opened = true;
+
+        if (this.onOpen && this.opened)
+            this.onOpen({});
+    }
+
+    close() {
+        if (this.disabled)
+            return;
+
+        this.opened = false;
+
+        if (this.onClose && !this.opened)
+            this.onClose({});
+    }
+}
diff --git a/modules/frontend/app/components/panel-collapsible/index.js b/modules/frontend/app/components/panel-collapsible/index.js
new file mode 100644
index 0000000..6f64b3b
--- /dev/null
+++ b/modules/frontend/app/components/panel-collapsible/index.js
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+import transclude from './transcludeDirective';
+
+export default angular
+    .module('ignite-console.panel-collapsible', [])
+    .directive('panelCollapsibleTransclude', transclude)
+    .component('panelCollapsible', component);
diff --git a/modules/frontend/app/components/panel-collapsible/index.spec.js b/modules/frontend/app/components/panel-collapsible/index.spec.js
new file mode 100644
index 0000000..94e9def
--- /dev/null
+++ b/modules/frontend/app/components/panel-collapsible/index.spec.js
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'mocha';
+import {assert} from 'chai';
+import angular from 'angular';
+import {spy} from 'sinon';
+import componentModule from './index';
+
+suite('panel-collapsible', () => {
+    const ICON_COLLAPSE = 'collapse';
+    const ICON_EXPAND = 'expand';
+    /** @type {ng.IScope} */
+    let $scope;
+    /** @type {ng.ICompileService} */
+    let $compile;
+    angular.module('test', [componentModule.name]);
+
+    const isClosed = (el) => el[0].querySelector('.panel-collapsible__content').classList.contains('ng-hide');
+    const click = (el) => el[0].querySelector('.panel-collapsible__status-icon').click();
+    const getIcon = (el) => (
+        el[0]
+            .querySelector('.panel-collapsible__status-icon [ignite-icon]:not(.ng-hide)')
+            .getAttribute('ignite-icon')
+    );
+
+    setup(() => {
+        angular.module('test', [componentModule.name]);
+        angular.mock.module('test');
+        angular.mock.inject((_$rootScope_, _$compile_) => {
+            $compile = _$compile_;
+            $scope = _$rootScope_.$new();
+        });
+    });
+
+    test('Required slot', () => {
+        const el = angular.element(`<panel-collapsible></panel-collapsible>`);
+        assert.throws(
+            () => $compile(el)($scope),
+            /Required transclusion slot `content` was not filled/,
+            'Throws when panel-content slot was not filled'
+        );
+    });
+
+    test('Open/close', () => {
+        $scope.opened = false;
+        const onOpen = $scope.onOpen = spy();
+        const onClose = $scope.onClose = spy();
+        const el = angular.element(`
+            <panel-collapsible
+                opened="opened"
+                on-open="onOpen()"
+                on-close="onClose()"
+            >
+                <panel-content>Content</panel-content>
+            </panel-collapsible>
+        `);
+
+        $compile(el)($scope);
+        $scope.$digest();
+        assert.equal(getIcon(el), ICON_EXPAND, `Shows ${ICON_EXPAND} icon when closed`);
+        assert.ok(isClosed(el), 'Hides content by default');
+        click(el);
+        $scope.$digest();
+        assert.equal(getIcon(el), ICON_COLLAPSE, `Shows ${ICON_COLLAPSE} icon when opened`);
+        assert.notOk(isClosed(el), 'Shows content when clicked');
+        click(el);
+        $scope.$digest();
+        assert.equal(onOpen.callCount, 1, 'Evaluates onOpen expression');
+        assert.equal(onClose.callCount, 1, 'Evaluates onClose expression');
+        $scope.opened = true;
+        $scope.$digest();
+        assert.notOk(isClosed(el), 'Uses opened binding to control visibility');
+    });
+
+    test('Slot transclusion', () => {
+        const el = angular.element(`
+            <panel-collapsible opened='::true'>
+                <panel-title>Title {{$panel.opened}}</panel-title>
+                <panel-description>Description {{$panel.opened}}</panel-description>
+                <panel-actions>
+                    <button
+                        class='my-button'
+                        ng-click='$panel.close()'
+                    >Button {{$panel.opened}}</button>
+                </panel-actions>
+                <panel-content>Content {{$panel.opened}}</panel-content>
+            </panel-collapsible>
+        `);
+        $compile(el)($scope);
+        $scope.$digest();
+        assert.equal(
+            el[0].querySelector('panel-title').textContent,
+            'Title true',
+            'Transcludes title slot and exposes $panel controller'
+        );
+        assert.equal(
+            el[0].querySelector('panel-description').textContent,
+            'Description true',
+            'Transcludes Description slot and exposes $panel controller'
+        );
+        assert.equal(
+            el[0].querySelector('panel-content').textContent,
+            'Content true',
+            'Transcludes content slot and exposes $panel controller'
+        );
+        el[0].querySelector('.my-button').click();
+        $scope.$digest();
+        assert.ok(isClosed(el), 'Can close by calling a method on exposed controller');
+    });
+
+    test('Disabled state', () => {
+        const el = angular.element(`
+            <panel-collapsible disabled='disabled'>
+                <panel-content>Content</panel-content>
+            </panel-collapsible>
+        `);
+        $compile(el)($scope);
+        $scope.$digest();
+        click(el);
+        $scope.$digest();
+        assert.ok(isClosed(el), `Can't be opened when disabled`);
+        // TODO: test disabled styles
+    });
+});
diff --git a/modules/frontend/app/components/panel-collapsible/style.scss b/modules/frontend/app/components/panel-collapsible/style.scss
new file mode 100644
index 0000000..748b99b
--- /dev/null
+++ b/modules/frontend/app/components/panel-collapsible/style.scss
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+panel-collapsible {
+    display: flex;
+    flex-direction: column;
+    border-radius: 0 0 4px 4px;
+    background-color: #ffffff;
+    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
+
+    &[disabled] {
+        opacity: 0.5;
+    }
+
+    .#{&}__status-icon {
+        margin-right: 10px;
+        color: #757575;
+    }
+
+    .#{&}__heading {
+        padding: 30px 20px 30px;
+        color: #393939;
+        display: flex;
+        flex-direction: row;
+        align-items: baseline;
+        user-select: none;
+        cursor: pointer;
+        line-height: 1.42857;
+    }
+
+    .#{&}__title {
+        font-size: 16px;
+        margin-right: 8px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+    }
+
+    .#{&}__description {
+        font-size: 12px;
+        color: #757575;
+        flex: 1 1;
+    }
+
+    .#{&}__actions {
+        margin-left: auto;
+        flex: 0 0 auto;
+
+        & > panel-actions {
+            display: flex;
+            flex-direction: row;
+
+            & > * {
+                flex: 0 0 auto;
+                margin-left: 10px;
+            }
+        }
+    }
+
+    .#{&}__content {
+        border-top: 1px solid #dddddd;
+        padding: 15px 20px;
+        contain: content;
+    }
+}
diff --git a/modules/frontend/app/components/panel-collapsible/template.pug b/modules/frontend/app/components/panel-collapsible/template.pug
new file mode 100644
index 0000000..9120f58
--- /dev/null
+++ b/modules/frontend/app/components/panel-collapsible/template.pug
@@ -0,0 +1,24 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.panel-collapsible__heading(ng-click='$ctrl.toggle()')
+    .panel-collapsible__status-icon
+        svg(ng-show='$ctrl.opened' ignite-icon='collapse')
+        svg(ng-hide='$ctrl.opened' ignite-icon='expand')
+    .panel-collapsible__title(panel-collapsible-transclude='title') {{::$ctrl.title}}
+    .panel-collapsible__description(panel-collapsible-transclude='description') {{::$ctrl.description}}
+    .panel-collapsible__actions(panel-collapsible-transclude='actions' ng-click='$event.stopPropagation()')
+.panel-collapsible__content(panel-collapsible-transclude='content' ng-show='$ctrl.opened')
\ No newline at end of file
diff --git a/modules/frontend/app/components/panel-collapsible/transcludeDirective.js b/modules/frontend/app/components/panel-collapsible/transcludeDirective.js
new file mode 100644
index 0000000..86509f5
--- /dev/null
+++ b/modules/frontend/app/components/panel-collapsible/transcludeDirective.js
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// eslint-disable-next-line
+import {default as Panel} from './controller';
+
+export default function panelCollapsibleTransclude() {
+    return {
+        restrict: 'A',
+        require: {
+            panel: '^panelCollapsible'
+        },
+        scope: true,
+        controller: class {
+            /** @type {Panel} */
+            panel;
+            /** @type {string} */
+            slot;
+            static $inject = ['$element'];
+            /**
+             * @param {JQLite} $element
+             */
+            constructor($element) {
+                this.$element = $element;
+            }
+            $postLink() {
+                this.panel.$transclude((clone, scope) => {
+                    scope.$panel = this.panel;
+                    this.$element.append(clone);
+                }, null, this.slot);
+            }
+        },
+        bindToController: {
+            slot: '@panelCollapsibleTransclude'
+        }
+    };
+}
diff --git a/modules/frontend/app/components/password-visibility/index.js b/modules/frontend/app/components/password-visibility/index.js
new file mode 100644
index 0000000..d735869
--- /dev/null
+++ b/modules/frontend/app/components/password-visibility/index.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import './style.scss';
+import {directive as visibilityRoot} from './root.directive';
+import {component as toggleButton} from './toggle-button.component';
+
+export default angular
+    .module('ignite-console.passwordVisibility', [])
+    .directive('passwordVisibilityRoot', visibilityRoot)
+    .component('passwordVisibilityToggleButton', toggleButton);
diff --git a/modules/frontend/app/components/password-visibility/index.spec.js b/modules/frontend/app/components/password-visibility/index.spec.js
new file mode 100644
index 0000000..f887092
--- /dev/null
+++ b/modules/frontend/app/components/password-visibility/index.spec.js
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'mocha';
+import {assert} from 'chai';
+import angular from 'angular';
+import module from './index';
+
+const PASSWORD_VISIBLE_CLASS = 'password-visibility__password-visible';
+
+suite('password-visibility', () => {
+    /** @type {ng.IScope} */
+    let $scope;
+    /** @type {ng.ICompileService} */
+    let $compile;
+
+    angular.module('test', [module.name]);
+
+    setup(() => {
+        angular.module('test', [module.name]);
+        angular.mock.module('test');
+        angular.mock.inject((_$rootScope_, _$compile_) => {
+            $compile = _$compile_;
+            $scope = _$rootScope_.$new();
+        });
+    });
+
+    test('Visibility toggle', () => {
+        const el = angular.element(`
+            <div password-visibility-root on-password-visibility-toggle='visible = $event'>
+                <password-visibility-toggle-button></password-visibility-toggle-button>
+            </div>
+        `);
+        $compile(el)($scope);
+        const toggleButton = el.find('password-visibility-toggle-button').children()[0];
+        $scope.$digest();
+
+        assert.isFalse(el.hasClass(PASSWORD_VISIBLE_CLASS), 'Password is hidden by default');
+
+        toggleButton.click();
+        $scope.$digest();
+
+        assert.isTrue(el.hasClass(PASSWORD_VISIBLE_CLASS), 'Password is visible after click on toggle button');
+        assert.equal(true, $scope.visible, 'Event emits current visibility value');
+
+        toggleButton.click();
+        $scope.$digest();
+
+        assert.isFalse(el.hasClass(PASSWORD_VISIBLE_CLASS), 'Password is hidden again after two clicks on button');
+    });
+});
diff --git a/modules/frontend/app/components/password-visibility/root.directive.js b/modules/frontend/app/components/password-visibility/root.directive.js
new file mode 100644
index 0000000..a041398
--- /dev/null
+++ b/modules/frontend/app/components/password-visibility/root.directive.js
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const PASSWORD_VISIBLE_CLASS = 'password-visibility__password-visible';
+
+export class PasswordVisibilityRoot {
+    /** @type {ng.ICompiledExpression} */
+    onPasswordVisibilityToggle;
+
+    isVisible = false;
+    static $inject = ['$element'];
+
+    /**
+     * @param {JQLite} $element
+     */
+    constructor($element) {
+        this.$element = $element;
+    }
+    toggleVisibility() {
+        this.isVisible = !this.isVisible;
+        this.$element.toggleClass(PASSWORD_VISIBLE_CLASS, this.isVisible);
+        if (this.onPasswordVisibilityToggle) this.onPasswordVisibilityToggle({$event: this.isVisible});
+    }
+}
+
+export function directive() {
+    return {
+        restrict: 'A',
+        scope: false,
+        controller: PasswordVisibilityRoot,
+        bindToController: {
+            onPasswordVisibilityToggle: '&?'
+        }
+    };
+}
diff --git a/modules/frontend/app/components/password-visibility/style.scss b/modules/frontend/app/components/password-visibility/style.scss
new file mode 100644
index 0000000..67457eb
--- /dev/null
+++ b/modules/frontend/app/components/password-visibility/style.scss
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+[password-visibility-root] {
+    &:not(.password-visibility__password-visible) {
+        .password-visibility__icon-visible,
+        .password-visibility__password-visible {
+            display: none;
+        }        
+    }
+
+    &.password-visibility__password-visible {
+        .password-visibility__icon-hidden,
+        .password-visibility__password-hidden {
+            display: none;
+        }
+    }
+}
+
+password-visibility-toggle-button {
+    display: inline-block;
+    width: 36px;
+
+    button {
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        width: 100%;
+        height: 100%;
+        background: none;
+        border: none;
+        outline: none;
+        padding: 0 !important;
+        margin: 0 !important;
+
+        &:focus {
+            color: #0067b9;
+        }
+    }
+}
diff --git a/modules/frontend/app/components/password-visibility/toggle-button.component.js b/modules/frontend/app/components/password-visibility/toggle-button.component.js
new file mode 100644
index 0000000..2e16201
--- /dev/null
+++ b/modules/frontend/app/components/password-visibility/toggle-button.component.js
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {PasswordVisibilityRoot} from './root.directive';
+
+class Controller {
+    /** @type {PasswordVisibilityRoot} */
+    visibilityRoot;
+
+    toggleVisibility() {
+        this.visibilityRoot.toggleVisibility();
+    }
+    get isVisible() {
+        return this.visibilityRoot.isVisible;
+    }
+}
+
+export const component = {
+    template: `
+        <button
+            type='button'
+            ng-click='$ctrl.toggleVisibility()'
+            bs-tooltip=''
+            data-title='{{ $ctrl.isVisible ? "Hide password" : "Show password" }}'
+            data-placement='top'
+        >
+            <svg ignite-icon='eyeOpened' class='password-visibility__icon-visible'></svg>
+            <svg ignite-icon='eyeClosed' class='password-visibility__icon-hidden'></svg>
+        </button>
+    `,
+    require: {
+        visibilityRoot: '^passwordVisibilityRoot'
+    },
+    controller: Controller
+};
diff --git a/modules/frontend/app/components/permanent-notifications/component.ts b/modules/frontend/app/components/permanent-notifications/component.ts
new file mode 100644
index 0000000..e86d3e5
--- /dev/null
+++ b/modules/frontend/app/components/permanent-notifications/component.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+import './style.scss';
+
+export const component: ng.IComponentOptions = {
+    template,
+    controller
+};
diff --git a/modules/frontend/app/components/permanent-notifications/controller.ts b/modules/frontend/app/components/permanent-notifications/controller.ts
new file mode 100644
index 0000000..ff0b182
--- /dev/null
+++ b/modules/frontend/app/components/permanent-notifications/controller.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class PermanentNotifications {
+    static $inject = ['UserNotifications', '$rootScope', '$window'];
+
+    constructor(
+        private UserNotifications: unknown,
+        private $rootScope: ng.IRootScopeService,
+        private $window: ng.IWindowService
+    ) {}
+
+    closeDemo() {
+        this.$window.close();
+    }
+}
diff --git a/modules/frontend/app/components/permanent-notifications/index.ts b/modules/frontend/app/components/permanent-notifications/index.ts
new file mode 100644
index 0000000..33a9189
--- /dev/null
+++ b/modules/frontend/app/components/permanent-notifications/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {component} from './component';
+
+export default angular.module('ignite-console.permanent-notifications', [])
+    .component('permanentNotifications', component);
diff --git a/modules/frontend/app/components/permanent-notifications/style.scss b/modules/frontend/app/components/permanent-notifications/style.scss
new file mode 100644
index 0000000..e40f21a
--- /dev/null
+++ b/modules/frontend/app/components/permanent-notifications/style.scss
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+permanent-notifications {
+    display: block;
+
+    @import "./../../../public/stylesheets/variables.scss";
+
+    .wch-notification {
+        line-height: 16px;
+        padding: 7px;
+        background: $brand-warning;
+        font-size: 16px;
+        text-align: center;
+
+        a {
+            color: $brand-info;
+        }
+
+        &+.wch-notification {
+            border-top: 1px solid darken($brand-warning, 15%);
+        }
+    }
+
+    .wch-notification.wch-notification--demo {
+        color: white;
+        background: $ignite-brand-success;
+        border: none;
+
+        a {
+            color: white;
+            text-decoration: underline;
+
+            &:hover {
+                color: #ffab40;
+                text-decoration: none;
+            }
+        }
+    }    
+}
\ No newline at end of file
diff --git a/modules/frontend/app/components/permanent-notifications/template.pug b/modules/frontend/app/components/permanent-notifications/template.pug
new file mode 100644
index 0000000..27bb297
--- /dev/null
+++ b/modules/frontend/app/components/permanent-notifications/template.pug
@@ -0,0 +1,23 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.wch-notification(ng-show='$ctrl.UserNotifications.isShown && $ctrl.UserNotifications.message' ng-bind-html='$ctrl.UserNotifications.message')
+
+.wch-notification(ng-show='$ctrl.$rootScope.user.becomeUsed')
+    | You are currently viewing user #[strong {{$ctrl.$rootScope.user.firstName}} {{$ctrl.$rootScope.user.lastName}}] as administrator. #[a(ng-click='$ctrl.$rootScope.revertIdentity()') Revert to your identity?]
+
+.wch-notification.wch-notification--demo(ng-if='$ctrl.$rootScope.IgniteDemoMode')
+    | You are now in Demo Mode. #[a(ng-click='$ctrl.closeDemo()') Close Demo?]
diff --git a/modules/frontend/app/components/progress-line/component.js b/modules/frontend/app/components/progress-line/component.js
new file mode 100644
index 0000000..f998ec8
--- /dev/null
+++ b/modules/frontend/app/components/progress-line/component.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './style.scss';
+import controller from './controller';
+import template from './template.pug';
+
+export default {
+    controller,
+    template,
+    bindings: {
+        value: '<?'
+    }
+};
diff --git a/modules/frontend/app/components/progress-line/controller.js b/modules/frontend/app/components/progress-line/controller.js
new file mode 100644
index 0000000..f383d59
--- /dev/null
+++ b/modules/frontend/app/components/progress-line/controller.js
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const INDETERMINATE_CLASS = 'progress-line__indeterminate';
+const COMPLETE_CLASS = 'progress-line__complete';
+
+/**
+ * @typedef {-1} IndeterminateValue
+ */
+
+/**
+ * @typedef {1} CompleteValue
+ */
+
+/**
+ * @typedef {IndeterminateValue|CompleteValue} ProgressLineValue
+ */
+
+export default class ProgressLine {
+    /** @type {ProgressLineValue} */
+    value;
+
+    static $inject = ['$element'];
+
+    /**
+     * @param {JQLite} $element
+     */
+    constructor($element) {
+        this.$element = $element;
+    }
+
+    /**
+     * @param {{value: ng.IChangesObject<ProgressLineValue>}} changes
+     */
+    $onChanges(changes) {
+        if (changes.value.currentValue === -1) {
+            this.$element[0].classList.remove(COMPLETE_CLASS);
+            this.$element[0].classList.add(INDETERMINATE_CLASS);
+            return;
+        }
+        if (typeof changes.value.currentValue === 'number') {
+            if (changes.value.currentValue === 1) this.$element[0].classList.add(COMPLETE_CLASS);
+            this.$element[0].classList.remove(INDETERMINATE_CLASS);
+        }
+    }
+}
diff --git a/modules/frontend/app/components/progress-line/index.js b/modules/frontend/app/components/progress-line/index.js
new file mode 100644
index 0000000..a91b97b
--- /dev/null
+++ b/modules/frontend/app/components/progress-line/index.js
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('ignite-console.progress-line', [])
+    .component('progressLine', component);
diff --git a/modules/frontend/app/components/progress-line/index.spec.js b/modules/frontend/app/components/progress-line/index.spec.js
new file mode 100644
index 0000000..f5725e9
--- /dev/null
+++ b/modules/frontend/app/components/progress-line/index.spec.js
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'mocha';
+import {assert} from 'chai';
+import angular from 'angular';
+import module from './index';
+
+const INDETERMINATE_CLASS = 'progress-line__indeterminate';
+const COMPLETE_CLASS = 'progress-line__complete';
+
+suite('progress-line', () => {
+    let $scope;
+    let $compile;
+
+    setup(() => {
+        angular.module('test', [module.name]);
+        angular.mock.module('test');
+        angular.mock.inject((_$rootScope_, _$compile_) => {
+            $compile = _$compile_;
+            $scope = _$rootScope_.$new();
+        });
+    });
+
+    test('Progress states', () => {
+        $scope.progress = -1;
+        const el = angular.element(`<progress-line value='progress'></progress-line>`);
+
+        $compile(el)($scope);
+        $scope.$digest();
+
+        assert.isTrue(
+            el[0].classList.contains(INDETERMINATE_CLASS),
+            'Adds indeterminate class for indeterminate state'
+        );
+
+        assert.isFalse(
+            el[0].classList.contains(COMPLETE_CLASS),
+            'Does not have complete class when in indeterminate state'
+        );
+
+        $scope.progress = 1;
+        $scope.$digest();
+
+        assert.isFalse(
+            el[0].classList.contains(INDETERMINATE_CLASS),
+            'Does not has indeterminate class when in finished state'
+        );
+
+        assert.isTrue(
+            el[0].classList.contains(COMPLETE_CLASS),
+            'Adds complete class when in finished state'
+        );
+    });
+});
diff --git a/modules/frontend/app/components/progress-line/style.scss b/modules/frontend/app/components/progress-line/style.scss
new file mode 100644
index 0000000..3b7f4ef
--- /dev/null
+++ b/modules/frontend/app/components/progress-line/style.scss
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+progress-line {
+    @import 'public/stylesheets/variables';
+
+    --background-color: transparent;
+    --foreground-color: #{$ignite-brand-primary};
+
+    height: 1px;
+    position: relative;
+    display: block;
+    overflow: hidden;
+
+    @keyframes progress-line-indeterminate {
+        0% {
+            left: -33%;
+            width: 33%;
+        }
+        100% {
+            left: 100%;
+            width: 33%;
+        }
+    }
+
+    @keyframes progress-line-indeterminate-to-complete {
+        0% {
+            opacity: 0;
+        }
+        100% {
+            opacity: 1;
+        }
+    }
+
+    .progress-line__background {
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        display: block;
+        background: var(--background-color);
+    }
+
+    .progress-line__foreground {
+        position: absolute;
+        top: 0;
+        bottom: 0;
+        content: "";
+        display: block;
+        background: var(--foreground-color);
+    }
+
+    &.progress-line__complete .progress-line__foreground {
+        animation-name: progress-line-indeterminate-to-complete;
+        animation-iteration-count: 1;
+        animation-duration: 0.2s;
+        left: 0;
+        right: 0;
+        width: 100%;
+    }
+
+    &.progress-line__indeterminate .progress-line__foreground {
+        animation-name: progress-line-indeterminate;
+        animation-iteration-count: infinite;
+        animation-duration: 2s;
+    }
+}
diff --git a/modules/frontend/app/components/progress-line/template.pug b/modules/frontend/app/components/progress-line/template.pug
new file mode 100644
index 0000000..b48beae
--- /dev/null
+++ b/modules/frontend/app/components/progress-line/template.pug
@@ -0,0 +1,18 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.progress-line__background
+.progress-line__foreground
diff --git a/modules/frontend/app/components/protect-from-bs-select-render/directive.js b/modules/frontend/app/components/protect-from-bs-select-render/directive.js
new file mode 100644
index 0000000..e51d477
--- /dev/null
+++ b/modules/frontend/app/components/protect-from-bs-select-render/directive.js
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default function protectFromBsSelectRender() {
+    return {
+        link(scope, el, attr, ctrl) {
+            const {$render} = ctrl;
+
+            Object.defineProperty(ctrl, '$render', {
+                set() {},
+                get() {
+                    return $render;
+                }
+            });
+        },
+        require: 'ngModel'
+    };
+}
diff --git a/modules/frontend/app/components/protect-from-bs-select-render/index.js b/modules/frontend/app/components/protect-from-bs-select-render/index.js
new file mode 100644
index 0000000..fd63000
--- /dev/null
+++ b/modules/frontend/app/components/protect-from-bs-select-render/index.js
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import directive from './directive';
+
+export default angular
+    .module('ignite-console.protect-from-bs-select-render', [])
+    .directive('protectFromBsSelectRender', directive);
diff --git a/modules/frontend/app/components/status-output/component.ts b/modules/frontend/app/components/status-output/component.ts
new file mode 100644
index 0000000..fed8280
--- /dev/null
+++ b/modules/frontend/app/components/status-output/component.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Status} from './controller';
+import templateUrl from './template.tpl.pug';
+import './style.scss';
+
+const component: ng.IComponentOptions = {
+    templateUrl,
+    bindings: {
+        options: '<',
+        value: '<'
+    },
+    controller: Status
+};
+
+export {component};
diff --git a/modules/frontend/app/components/status-output/componentFactory.ts b/modules/frontend/app/components/status-output/componentFactory.ts
new file mode 100644
index 0000000..192abb5
--- /dev/null
+++ b/modules/frontend/app/components/status-output/componentFactory.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {StatusOptions} from './index';
+import {Status} from './controller';
+import {component} from './component';
+
+export const componentFactory = (options: StatusOptions) => ({
+    ...component,
+    bindings: {
+        value: '<'
+    },
+    controller: class extends Status {
+        options = options
+    }
+});
diff --git a/modules/frontend/app/components/status-output/controller.ts b/modules/frontend/app/components/status-output/controller.ts
new file mode 100644
index 0000000..f5a8179
--- /dev/null
+++ b/modules/frontend/app/components/status-output/controller.ts
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {StatusOption, StatusOptions} from './index';
+
+interface Changes extends ng.IOnChangesObject {
+    value: ng.IChangesObject<string>,
+    options: ng.IChangesObject<StatusOptions>
+}
+
+const UNIVERSAL_CLASSNAME = 'status-output';
+
+export class Status implements ng.IComponentController, ng.IOnChanges, ng.IPostLink, ng.IOnDestroy {
+    static $inject = ['$element'];
+
+    value: string;
+    options: StatusOptions;
+    status: StatusOption | undefined;
+    statusClassName: string | undefined;
+
+    constructor(private el: JQLite) {}
+
+    $postLink() {
+        this.el[0].classList.add(UNIVERSAL_CLASSNAME);
+    }
+
+    $onDestroy() {
+        delete this.el;
+    }
+
+    $onChanges(changes: Changes) {
+        if ('value' in changes) {
+            this.status = this.options.find((option) => option.value === this.value);
+
+            if (this.status)
+                this.statusClassName = `${UNIVERSAL_CLASSNAME}__${this.status.level.toLowerCase()}`;
+        }
+    }
+}
diff --git a/modules/frontend/app/components/status-output/index.ts b/modules/frontend/app/components/status-output/index.ts
new file mode 100644
index 0000000..0c902b2
--- /dev/null
+++ b/modules/frontend/app/components/status-output/index.ts
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {component} from './component';
+
+export {componentFactory} from './componentFactory';
+
+export interface StatusOption {
+    level: StatusLevel,
+    // Whether to show progress indicator or not
+    ongoing?: boolean,
+    value: string | boolean,
+    // What user will see
+    label: string
+}
+
+export type StatusOptions = Array<StatusOption>;
+
+export enum StatusLevel {
+    NEUTRAL = 'NEUTRAL',
+    GREEN = 'GREEN',
+    RED = 'RED'
+}
+
+export default angular
+    .module('ignite-console.components.status-output', [])
+    .component('statusOutput', component);
diff --git a/modules/frontend/app/components/status-output/style.scss b/modules/frontend/app/components/status-output/style.scss
new file mode 100644
index 0000000..d49213f
--- /dev/null
+++ b/modules/frontend/app/components/status-output/style.scss
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.status-output {
+    @import 'public/stylesheets/variables';
+
+    display: inline-flex;
+
+    .spinner-circle {
+        margin-left: 5px;
+    }
+
+    .status-output__neutral {
+        color: inherit;
+    }
+
+    .status-output__green {
+        color: $ignite-status-active;
+    }
+
+    .status-output__red {
+        color: $ignite-status-inactive;
+    }
+}
diff --git a/modules/frontend/app/components/status-output/template.tpl.pug b/modules/frontend/app/components/status-output/template.tpl.pug
new file mode 100644
index 0000000..a79035e
--- /dev/null
+++ b/modules/frontend/app/components/status-output/template.tpl.pug
@@ -0,0 +1,18 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+span(translate='{{$ctrl.status.label}}' ng-class='$ctrl.statusClassName')
+.spinner-circle(ng-if='$ctrl.status.ongoing')
diff --git a/modules/frontend/app/components/timed-redirection/component.ts b/modules/frontend/app/components/timed-redirection/component.ts
new file mode 100644
index 0000000..5cab0c9
--- /dev/null
+++ b/modules/frontend/app/components/timed-redirection/component.ts
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+import {TimedRedirectionCtrl} from './controller';
+
+export const component: ng.IComponentOptions = {
+    template,
+    controller: TimedRedirectionCtrl,
+    bindings: {
+        headerText: '<',
+        subHeaderText: '<'
+    }
+};
diff --git a/modules/frontend/app/components/timed-redirection/controller.ts b/modules/frontend/app/components/timed-redirection/controller.ts
new file mode 100644
index 0000000..c8fa892
--- /dev/null
+++ b/modules/frontend/app/components/timed-redirection/controller.ts
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {StateOrName, StateService} from '@uirouter/angularjs';
+import {RawParams} from '@uirouter/core/lib/params/interface';
+import {default as UserFactory} from 'app/modules/user/User.service';
+
+interface State {
+    name: StateOrName,
+    params: RawParams
+}
+
+export class TimedRedirectionCtrl implements ng.IComponentController, ng.IOnInit, ng.IOnDestroy {
+    static $inject = ['$state', '$interval', 'User'];
+
+    lastSuccessState = JSON.parse(localStorage.getItem('lastStateChangeSuccess'));
+
+    stateToGo: State = this.lastSuccessState || {name: 'default-state', params: {}};
+
+    secondsLeft: number = 10;
+
+    countDown: ng.IPromise<ng.IIntervalService>;
+
+    constructor(private $state: StateService, private $interval: ng.IIntervalService, private user: ReturnType<typeof UserFactory>) {}
+
+    $onInit() {
+        this.startCountDown();
+    }
+
+    $onDestroy() {
+        this.$interval.cancel(this.countDown);
+    }
+
+    async go(): void {
+        try {
+            await this.user.load();
+
+            this.$state.go(this.stateToGo.name, this.stateToGo.params);
+        }
+        catch (ignored) {
+            this.$state.go('signin');
+        }
+    }
+
+    startCountDown(): void {
+        this.countDown = this.$interval(() => {
+            this.secondsLeft--;
+
+            if (this.secondsLeft === 0)
+                this.go();
+
+        }, 1000, this.secondsLeft);
+    }
+}
diff --git a/modules/frontend/app/components/timed-redirection/index.ts b/modules/frontend/app/components/timed-redirection/index.ts
new file mode 100644
index 0000000..c4951f6
--- /dev/null
+++ b/modules/frontend/app/components/timed-redirection/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as angular from 'angular';
+
+import {component} from './component';
+
+export default angular.module('ignite-console.timed-redirection', [])
+    .component('timedRedirection', component);
diff --git a/modules/frontend/app/components/timed-redirection/style.scss b/modules/frontend/app/components/timed-redirection/style.scss
new file mode 100644
index 0000000..487fa2d
--- /dev/null
+++ b/modules/frontend/app/components/timed-redirection/style.scss
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import "./../../../public/stylesheets/variables.scss";
+
+timed-redirection {
+  justify-content: center;
+  align-items: center;
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  min-height: 100%;
+
+  .timed-redirection--wrapper {
+      text-align: center;
+  }
+
+  h1, .sub-header {
+    color: $ignite-brand-primary;
+  }
+
+  h1 {
+    font-size: 64px;
+  }
+
+  .sub-header {
+    font-size: 30px;
+  }
+
+  .redirection-text {
+    margin-top: 30px;
+    font-size: 16px;
+  }
+}
diff --git a/modules/frontend/app/components/timed-redirection/template.pug b/modules/frontend/app/components/timed-redirection/template.pug
new file mode 100644
index 0000000..77f8298
--- /dev/null
+++ b/modules/frontend/app/components/timed-redirection/template.pug
@@ -0,0 +1,22 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.timed-redirection--wrapper
+    h1 {{::$ctrl.headerText}}
+
+    .sub-header {{::$ctrl.subHeaderText}}
+
+    .redirection-text You’ll be redirected back automatically in {{$ctrl.secondsLeft}} seconds, or #[a(ng-click='$ctrl.go()') click here] to redirect now.
diff --git a/modules/frontend/app/components/ui-grid-column-resizer/directive.js b/modules/frontend/app/components/ui-grid-column-resizer/directive.js
new file mode 100644
index 0000000..6ba2a78
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid-column-resizer/directive.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default function() {
+    return {
+        priority: -200,
+        restrict: 'A',
+        require: '?^uiGrid',
+        link($scope, $element) {
+            $element.on('dblclick', function($event) {
+                $event.stopImmediatePropagation();
+            });
+        }
+    };
+}
diff --git a/modules/frontend/app/components/ui-grid-column-resizer/index.js b/modules/frontend/app/components/ui-grid-column-resizer/index.js
new file mode 100644
index 0000000..9edf1ef
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid-column-resizer/index.js
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import uiGridColumnResizer from './directive';
+
+export default angular
+    .module('ignite-console.ui-grid-column-resizer', [])
+    .directive('uiGridColumnResizer', uiGridColumnResizer);
diff --git a/modules/frontend/app/components/ui-grid-filters/directive.js b/modules/frontend/app/components/ui-grid-filters/directive.js
new file mode 100644
index 0000000..e22530b
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid-filters/directive.js
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+
+export default function uiGridFilters(uiGridConstants) {
+    return {
+        require: 'uiGrid',
+        link: {
+            pre(scope, el, attr, gridApi) {
+                if (!gridApi.grid.options.enableFiltering)
+                    return;
+
+                const applyMultiselectFilter = (cd) => {
+                    if (!cd.headerCellTemplate)
+                        cd.headerCellTemplate = template;
+
+                    cd.filter = {
+                        type: uiGridConstants.filter.SELECT,
+                        term: cd.multiselectFilterOptions.map((t) => t.value),
+                        condition(searchTerm, cellValue) {
+                            if (cellValue)
+                                return Array.isArray(cellValue) ? _.intersection(searchTerm, cellValue).length : searchTerm.includes(cellValue);
+
+                            return true;
+                        },
+                        selectOptions: cd.multiselectFilterOptions,
+                        $$selectOptionsMapping: cd.multiselectFilterOptions.reduce((a, v) => Object.assign(a, {[v.value]: v.label}), {}),
+                        $$multiselectFilterTooltip() {
+                            const prefix = 'Active filter';
+                            switch (this.term.length) {
+                                case 0:
+                                    return `${prefix}: show none`;
+                                default:
+                                    return `${prefix}: ${this.term.map((t) => this.$$selectOptionsMapping[t]).join(', ')}`;
+                                case this.selectOptions.length:
+                                    return `${prefix}: show all`;
+                            }
+                        }
+                    };
+                    if (!cd.cellTemplate) {
+                        cd.cellTemplate = `
+                            <div class="ui-grid-cell-contents">
+                                {{ col.colDef.filter.$$selectOptionsMapping[row.entity[col.field]] }}
+                            </div>
+                        `;
+                    }
+                };
+
+                const updateMultiselectOptionsHandler = (gridApi, colDef) => {
+                    if (!gridApi)
+                        return;
+
+                    const col = gridApi.grid.getColumn(colDef.name);
+                    const selectOptions = colDef.multiselectFilterOptionsFn(gridApi.grid, col.filter);
+
+                    if (selectOptions.length === col.filter.selectOptions.length)
+                        return;
+
+                    col.filter.term = selectOptions.map((t) => t.value);
+                    col.filter.selectOptions = selectOptions;
+                };
+
+                gridApi.grid.options.columnDefs.filter((cd) => cd.multiselectFilterOptions).forEach(applyMultiselectFilter);
+
+                gridApi.grid.options.columnDefs.filter((cd) => cd.multiselectFilterOptionsFn).forEach((cd) => {
+                    cd.multiselectFilterOptions = cd.multiselectFilterOptions || [];
+                    applyMultiselectFilter(cd);
+
+                    if (cd.multiselectFilterDialog)
+                        cd.filter.selectDialog = cd.multiselectFilterDialog;
+
+                    gridApi.grid.api.core.on.rowsVisibleChanged(scope, (gridApi) => updateMultiselectOptionsHandler(gridApi, cd));
+                });
+            }
+        }
+    };
+}
+
+uiGridFilters.$inject = ['uiGridConstants'];
diff --git a/modules/frontend/app/components/ui-grid-filters/index.js b/modules/frontend/app/components/ui-grid-filters/index.js
new file mode 100644
index 0000000..2c60d89
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid-filters/index.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import directive from './directive';
+import flow from 'lodash/flow';
+
+export default angular
+    .module('ignite-console.ui-grid-filters', ['ui.grid'])
+    .decorator('$tooltip', ['$delegate', ($delegate) => {
+        return function(el, config) {
+            const instance = $delegate(el, config);
+            instance.$referenceElement = el;
+            instance.destroy = flow(instance.destroy, () => instance.$referenceElement = null);
+            instance.$applyPlacement = flow(instance.$applyPlacement, () => {
+                if (!instance.$element)
+                    return;
+
+                const refWidth = instance.$referenceElement[0].getBoundingClientRect().width;
+                const elWidth = instance.$element[0].getBoundingClientRect().width;
+                if (refWidth > elWidth) {
+                    instance.$element.css({
+                        width: refWidth,
+                        maxWidth: 'initial'
+                    });
+                }
+            });
+            return instance;
+        };
+    }])
+    .directive('uiGridFilters', directive);
diff --git a/modules/frontend/app/components/ui-grid-filters/style.scss b/modules/frontend/app/components/ui-grid-filters/style.scss
new file mode 100644
index 0000000..a8d7a84
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid-filters/style.scss
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.ui-grid-filters[role="columnheader"] {
+	display: flex;
+    flex-wrap: nowrap !important;
+
+    // Decrease horizontal padding because multiselect button already has it
+    padding-left: 8px !important;
+    padding-right: 8px !important;
+
+    & > div:first-child {
+    	flex: auto !important;
+    }
+
+    .uigf-btn {
+        font-weight: normal;
+
+        &--active {
+            font-weight: bold;
+        }
+    }
+
+    .ui-grid-cell-contents[role="button"] {
+        flex: auto !important;
+        flex-basis: 100% !important;
+
+        padding: 0 !important;
+        margin-left: -10px;
+        overflow: visible !important;
+
+        font-weight: normal;
+    }
+}
diff --git a/modules/frontend/app/components/ui-grid-filters/template.pug b/modules/frontend/app/components/ui-grid-filters/template.pug
new file mode 100644
index 0000000..a865395
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid-filters/template.pug
@@ -0,0 +1,57 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.ui-grid-filter-container.ui-grid-filters(role='columnheader')
+    div(ng-style='col.extraStyle'
+        ng-repeat='colFilter in col.filters'
+        ng-class="{'ui-grid-filter-cancel-button-hidden' : colFilter.disableCancelFilterButton === true }"
+        ng-switch='colFilter.type')
+        div(ng-switch-when='select')
+            button.btn-ignite.btn-ignite--link-dashed-success.uigf-btn(
+                ng-class=`{
+                    'uigf-btn--active': colFilter.term.length !== colFilter.selectOptions.length
+                }`
+                type='button'
+                title='{{ colFilter.$$multiselectFilterTooltip() }}'
+                ng-model='colFilter.term'
+                ng-disabled='col.colDef.multiselectFilterDisabled'
+                bs-select
+                bs-options='option.value as option.label for option in colFilter.selectOptions'
+                data-multiple='true'
+                data-trigger='click'
+                data-placement='bottom-left'
+                protect-from-bs-select-render
+            ) {{ col.displayName }}
+        div(ng-switch-when='dialog')
+            button.btn-ignite.btn-ignite--link-dashed-success.uigf-btn(
+                ng-class=`{
+                    'uigf-btn--active': colFilter.term.length !== colFilter.selectOptions.length
+                }`
+                ng-click='colFilter.selectDialog(grid, colFilter)'
+                type='button'
+                title='{{ colFilter.$$multiselectFilterTooltip() }}'
+            ) {{ col.displayName }}
+
+    .ui-grid-cell-contents(role='button')
+        button.btn-ignite.btn-ignite--link-dashed-success(
+            ui-grid-one-bind-id-grid="col.uid + '-sortdir-text'"
+            ui-grid-visible="col.sort.direction"
+            aria-label="Sort Descending")
+            i(ng-class="{\
+                'ui-grid-icon-up-dir': col.sort.direction == 'asc',\
+                'ui-grid-icon-down-dir': col.sort.direction == 'desc',\
+                'ui-grid-icon-blank': !col.sort.direction\
+            }" title="" aria-hidden="true")
diff --git a/modules/frontend/app/components/ui-grid-hovering/cell.js b/modules/frontend/app/components/ui-grid-hovering/cell.js
new file mode 100644
index 0000000..f64df2c
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid-hovering/cell.js
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default function() {
+    return {
+        priority: -200,
+        restrict: 'A',
+        require: '?^uiGrid',
+        link($scope, $element) {
+            if (!$scope.grid.options.enableHovering)
+                return;
+
+            // Apply hover when mousing in.
+            $element.on('mouseover', () => {
+                // Empty all isHovered because scroll breaks it.
+                $scope.row.grid.api.core.getVisibleRows().forEach((row) => {
+                    row.isHovered = false;
+                });
+
+                // Now set proper hover
+                $scope.row.isHovered = true;
+
+                $scope.$apply();
+            });
+
+            // Remove hover when mousing out.
+            $element.on('mouseout', () => {
+                $scope.row.isHovered = false;
+
+                $scope.$apply();
+            });
+        }
+    };
+}
diff --git a/modules/frontend/app/components/ui-grid-hovering/hovering.js b/modules/frontend/app/components/ui-grid-hovering/hovering.js
new file mode 100644
index 0000000..17202a4
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid-hovering/hovering.js
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default function() {
+    return {
+        priority: 0,
+        require: '^uiGrid',
+        compile() {
+            return {
+                pre($scope, $element, attrs, uiGridCtrl) {
+                    uiGridCtrl.grid.options.enableHovering = true;
+                },
+                post() { }
+            };
+        }
+    };
+}
diff --git a/modules/frontend/app/components/ui-grid-hovering/index.js b/modules/frontend/app/components/ui-grid-hovering/index.js
new file mode 100644
index 0000000..eaa8207
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid-hovering/index.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import uiGridCell from './cell';
+import uiGridHovering from './hovering';
+import uiGridViewport from './viewport';
+
+import './style.scss';
+
+export default angular
+    .module('ignite-console.ui-grid-hovering', [])
+    .directive('uiGridCell', uiGridCell)
+    .directive('uiGridHovering', uiGridHovering)
+    .directive('uiGridViewport', uiGridViewport);
diff --git a/modules/frontend/app/components/ui-grid-hovering/style.scss b/modules/frontend/app/components/ui-grid-hovering/style.scss
new file mode 100644
index 0000000..d660045
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid-hovering/style.scss
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.ui-grid.ui-grid--ignite {
+    .ui-grid-row.ui-grid-row-hovered > [ui-grid-row] > .ui-grid-cell {
+        background: #ededed;
+    }
+}
+
+.grid[ui-grid-hovering='ui-grid-hovering'] {
+    .ui-grid-row.ui-grid-row-hovered > [ui-grid-row] > .ui-grid-cell {
+        background: #ededed;
+    }
+}
diff --git a/modules/frontend/app/components/ui-grid-hovering/viewport.js b/modules/frontend/app/components/ui-grid-hovering/viewport.js
new file mode 100644
index 0000000..7ef433a
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid-hovering/viewport.js
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+export default function() {
+    return {
+        priority: -200,
+        compile($el) {
+            let newNgClass = '';
+
+            const rowRepeatDiv = angular.element($el.children().children()[0]);
+            const existingNgClass = rowRepeatDiv.attr('ng-class');
+
+            if (existingNgClass)
+                newNgClass = existingNgClass.slice(0, -1) + ', "ui-grid-row-hovered": row.isHovered }';
+            else
+                newNgClass = '{ "ui-grid-row-hovered": row.isHovered }';
+
+            rowRepeatDiv.attr('ng-class', newNgClass);
+
+            return {
+                pre() { },
+                post() { }
+            };
+        }
+    };
+}
diff --git a/modules/frontend/app/components/ui-grid/component.js b/modules/frontend/app/components/ui-grid/component.js
new file mode 100644
index 0000000..cb9a927
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid/component.js
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './style.scss';
+import template from './template.pug';
+import controller from './controller';
+
+export default {
+    template,
+    controller,
+    bindings: {
+        gridApi: '=?',
+        gridTreeView: '<?',
+        gridGrouping: '<?',
+        gridThin: '<?',
+        gridHeight: '<?',
+        tabName: '<?',
+        tableTitle: '<?',
+        maxRowsToShow: '<?',
+
+        // Input Events.
+        items: '<',
+        columnDefs: '<',
+        categories: '<?',
+        singleSelect: '<?',
+        oneWaySelection: '<?',
+        rowIdentityKey: '@?',
+        selectedRows: '<?',
+        selectedRowsId: '<?',
+
+        // Output events.
+        onSelectionChange: '&?',
+        onApiRegistered: '&?'
+    }
+};
diff --git a/modules/frontend/app/components/ui-grid/controller.js b/modules/frontend/app/components/ui-grid/controller.js
new file mode 100644
index 0000000..230b9da
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid/controller.js
@@ -0,0 +1,244 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import debounce from 'lodash/debounce';
+import headerTemplate from 'app/primitives/ui-grid-header/index.tpl.pug';
+
+import ResizeObserver from 'resize-observer-polyfill';
+
+export default class IgniteUiGrid {
+    /** @type {import('ui-grid').IGridOptions} */
+    grid;
+
+    /** @type */
+    gridApi;
+
+    /** @type */
+    gridThin;
+
+    /** @type */
+    gridHeight;
+
+    /** @type */
+    items;
+
+    /** @type */
+    columnDefs;
+
+    /** @type */
+    categories;
+
+    /** @type {boolean} */
+    singleSelect;
+
+    /** @type */
+    onSelectionChange;
+
+    /** @type */
+    selectedRows;
+
+    /** @type */
+    selectedRowsId;
+
+    /** @type */
+    _selected;
+
+    static $inject = ['$scope', '$element', '$timeout', 'gridUtil'];
+
+    /**
+     * @param {ng.IScope} $scope
+     * @param $element
+     * @param $timeout
+     * @param gridUtil
+     */
+    constructor($scope, $element, $timeout, gridUtil) {
+        this.$scope = $scope;
+        this.$element = $element;
+        this.$timeout = $timeout;
+        this.gridUtil = gridUtil;
+
+        this.rowIdentityKey = '_id';
+
+        this.rowHeight = 48;
+        this.headerRowHeight = 70;
+    }
+
+    $onInit() {
+        this.SCROLLBAR_WIDTH = this.gridUtil.getScrollbarWidth();
+
+        if (this.gridThin) {
+            this.rowHeight = 36;
+            this.headerRowHeight = 48;
+        }
+
+        this.grid = {
+            appScopeProvider: this.$scope.$parent,
+            data: this.items,
+            columnDefs: this.columnDefs,
+            categories: this.categories,
+            rowHeight: this.rowHeight,
+            multiSelect: !this.singleSelect,
+            enableSelectAll: !this.singleSelect,
+            headerRowHeight: this.headerRowHeight,
+            columnVirtualizationThreshold: 30,
+            enableColumnMenus: false,
+            enableFullRowSelection: true,
+            enableFiltering: true,
+            enableRowHashing: false,
+            fastWatch: true,
+            showTreeExpandNoChildren: false,
+            modifierKeysToMultiSelect: true,
+            selectionRowHeaderWidth: 52,
+            exporterCsvColumnSeparator: ';',
+            onRegisterApi: (api) => {
+                this.gridApi = api;
+
+                api.core.on.rowsVisibleChanged(this.$scope, () => {
+                    this.adjustHeight();
+
+                    // Without property existence check non-set selectedRows or selectedRowsId
+                    // binding might cause unwanted behavior,
+                    // like unchecking rows during any items change,
+                    // even if nothing really changed.
+                    if (this._selected && this._selected.length && this.onSelectionChange) {
+                        this.applyIncomingSelectionRows(this._selected);
+
+                        // Change selected rows if filter was changed.
+                        this.onRowsSelectionChange([]);
+                    }
+                });
+
+                if (this.onSelectionChange) {
+                    api.selection.on.rowSelectionChanged(this.$scope, (row, e) => {
+                        this.onRowsSelectionChange([row], e);
+                    });
+
+                    api.selection.on.rowSelectionChangedBatch(this.$scope, (rows, e) => {
+                        this.onRowsSelectionChange(rows, e);
+                    });
+                }
+
+                api.core.on.filterChanged(this.$scope, (column) => {
+                    this.onFilterChange(column);
+                });
+
+                this.$timeout(() => {
+                    if (this.selectedRowsId) this.applyIncomingSelectionRowsId(this.selectedRowsId);
+                });
+
+                this.resizeObserver = new ResizeObserver(() => api.core.handleWindowResize());
+                this.resizeObserver.observe(this.$element[0]);
+
+                if (this.onApiRegistered)
+                    this.onApiRegistered({$event: api});
+            }
+        };
+
+        if (this.grid.categories)
+            this.grid.headerTemplate = headerTemplate;
+    }
+
+    $onChanges(changes) {
+        const hasChanged = (binding) =>
+            binding in changes && changes[binding].currentValue !== changes[binding].previousValue;
+
+        if (hasChanged('items') && this.grid)
+            this.grid.data = changes.items.currentValue;
+
+        if (hasChanged('selectedRows') && this.grid && this.grid.data && this.onSelectionChange)
+            this.applyIncomingSelectionRows(changes.selectedRows.currentValue);
+
+        if (hasChanged('selectedRowsId') && this.grid && this.grid.data)
+            this.applyIncomingSelectionRowsId(changes.selectedRowsId.currentValue);
+
+        if (hasChanged('gridHeight') && this.grid)
+            this.adjustHeight();
+    }
+
+    $onDestroy() {
+        if (this.resizeObserver)
+            this.resizeObserver.disconnect();
+    }
+
+    applyIncomingSelectionRows = (selected = []) => {
+        this.gridApi.selection.clearSelectedRows({ ignore: true });
+
+        const visibleRows = this.gridApi.core.getVisibleRows(this.gridApi.grid)
+            .map(({ entity }) => entity);
+
+        const rows = visibleRows.filter((r) =>
+            selected.map((row) => row[this.rowIdentityKey]).includes(r[this.rowIdentityKey]));
+
+        rows.forEach((r) => {
+            this.gridApi.selection.selectRow(r, { ignore: true });
+        });
+    };
+
+    applyIncomingSelectionRowsId = (selected = []) => {
+        if (this.onSelectionChange) {
+            this.gridApi.selection.clearSelectedRows({ ignore: true });
+
+            const visibleRows = this.gridApi.core.getVisibleRows(this.gridApi.grid)
+                .map(({ entity }) => entity);
+
+            const rows = visibleRows.filter((r) =>
+                selected.includes(r[this.rowIdentityKey]));
+
+            rows.forEach((r) => {
+                this.gridApi.selection.selectRow(r, { ignore: true });
+            });
+        }
+    };
+
+    onRowsSelectionChange = debounce((rows, e = {}) => {
+        if (e.ignore)
+            return;
+
+        this._selected = this.gridApi.selection.legacyGetSelectedRows();
+
+        if (this.onSelectionChange)
+            this.onSelectionChange({ $event: this._selected });
+    });
+
+    onFilterChange = debounce((column) => {
+        if (!this.gridApi.selection)
+            return;
+
+        if (this.selectedRows && this.onSelectionChange)
+            this.applyIncomingSelectionRows(this.selectedRows);
+
+        if (this.selectedRowsId)
+            this.applyIncomingSelectionRowsId(this.selectedRowsId);
+    });
+
+    adjustHeight() {
+        let height = this.gridHeight;
+
+        if (!height) {
+            const maxRowsToShow = this.maxRowsToShow || 5;
+            const headerBorder = 1;
+            const visibleRows = this.gridApi.core.getVisibleRows().length;
+            const header = this.grid.headerRowHeight + headerBorder;
+            const optionalScroll = (visibleRows ? this.gridUtil.getScrollbarWidth() : 0);
+
+            height = Math.min(visibleRows, maxRowsToShow) * this.grid.rowHeight + header + optionalScroll;
+        }
+
+        this.gridApi.grid.element.css('height', height + 'px');
+        this.gridApi.core.handleWindowResize();
+    }
+}
diff --git a/modules/frontend/app/components/ui-grid/decorator.js b/modules/frontend/app/components/ui-grid/decorator.js
new file mode 100644
index 0000000..a82f702
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid/decorator.js
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default ['$delegate', 'uiGridSelectionService', ($delegate, uiGridSelectionService) => {
+    $delegate[0].require = ['^uiGrid', '?^igniteGridTable'];
+    $delegate[0].compile = () => ($scope, $el, $attr, [uiGridCtrl, igniteGridTable]) => {
+        const self = uiGridCtrl.grid;
+
+        $delegate[0].link($scope, $el, $attr, uiGridCtrl);
+
+        const mySelectButtonClick = (row, evt) => {
+            evt.stopPropagation();
+
+            if (evt.shiftKey)
+                uiGridSelectionService.shiftSelect(self, row, evt, self.options.multiSelect);
+            else
+                uiGridSelectionService.toggleRowSelection(self, row, evt, self.options.multiSelect, self.options.noUnselect);
+        };
+
+        if (igniteGridTable)
+            $scope.selectButtonClick = mySelectButtonClick;
+    };
+    return $delegate;
+}];
diff --git a/modules/frontend/app/components/ui-grid/index.js b/modules/frontend/app/components/ui-grid/index.js
new file mode 100644
index 0000000..fce5268
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid/index.js
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+import decorator from './decorator';
+
+export default angular
+    .module('ignite-console.ui-grid', [])
+    .component('igniteGridTable', component)
+    .decorator('uiGridSelectionRowHeaderButtonsDirective', decorator);
diff --git a/modules/frontend/app/components/ui-grid/style.scss b/modules/frontend/app/components/ui-grid/style.scss
new file mode 100644
index 0000000..85ce77b
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid/style.scss
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.ignite-grid-table,
+ignite-grid-table {
+	@import 'public/stylesheets/variables';
+    display: block;
+
+    .ui-grid.ui-grid--ignite.ui-grid--thin {
+        // Start section row height.
+        .ui-grid-row {
+            height: 36px;
+
+            .ui-grid-cell {
+                height: 100%;
+            }
+        }
+
+        .ui-grid-cell .ui-grid-cell-contents {
+            padding: 8px 20px;
+            min-height: 35px;
+            max-height: 35px;
+        }
+
+        // Set force header height.
+        // Fix hide border bottom of pinned column without data.
+        .ui-grid-header-canvas {
+            height: 48px;
+        }
+    }
+
+    .ui-grid.ui-grid--ignite:not(.ui-grid--thin) {
+        // Start section row height.
+        .ui-grid-row {
+            height: 48px;
+
+            .ui-grid-cell {
+                height: 100%;
+            }
+        }
+
+        .ui-grid-cell .ui-grid-cell-contents {
+            padding: 14px 20px;
+            min-height: 47px;
+            max-height: 47px;
+        }
+
+        // Set force header height.
+        // Fix hide border bottom of pinned column without data.
+        .ui-grid-header-canvas {
+            height: 70px;
+        }
+
+        [role="columnheader"] {
+            margin: 11px 0;
+        }
+
+        // Fix checkbox position.
+        .ui-grid-header-cell  .ui-grid-selection-row-header-buttons {
+            margin-top: 12px;
+        }
+    }
+
+    .ui-grid.ui-grid--ignite {
+        .ui-grid-header .ui-grid-tree-base-row-header-buttons.ui-grid-icon-plus-squared,
+        .ui-grid-header .ui-grid-tree-base-row-header-buttons.ui-grid-icon-minus-squared {
+            top: 14px;
+        }
+
+        [role="columnheader"] {
+            display: flex;
+            align-items: center;
+        }
+
+        .ui-grid-header--subcategories [role="columnheader"] {
+            margin: 0;
+            background-color: white;
+        }
+
+        // Removes unwanted box-shadow and border-right from checkboxes column
+        .ui-grid-pinned-container.ui-grid-pinned-container-left .ui-grid-render-container-left:before {
+            box-shadow: none;
+        }
+        .ui-grid-pinned-container.ui-grid-pinned-container-left .ui-grid-cell:last-child {
+            border-right: none;
+        }
+
+        .ui-grid-pinned-container-left .ui-grid-header--subcategories .ui-grid-header-span.ui-grid-header-cell {
+            box-shadow: none;
+        }
+
+        .ui-grid-header:not(.ui-grid-header--subcategories) .ui-grid-filters[role="columnheader"] {
+            padding-top: 6px;
+        }
+
+        // End section row height.
+        .ui-grid-header-cell:last-child .ui-grid-column-resizer.right {
+            border-right: none;
+        }
+
+        input[type="text"].ui-grid-filter-input {
+            &::placeholder {
+                color: rgba(66, 66, 66, 0.5);
+                font-weight: normal;
+                text-align: left;
+            }
+
+            &:focus {
+                border-color: $ignite-brand-success;
+                box-shadow: none;
+            }
+
+            outline: none;
+            overflow: visible;
+
+            box-sizing: border-box;
+            width: 100%;
+            max-width: initial;
+            height: 29px;
+            padding: 0 10px;
+            margin-right: 0;
+
+            border: solid 1px #c5c5c5;
+            border-radius: 4px;
+            background-color: #ffffff;
+            box-shadow: none;
+
+            color: $text-color;
+            text-align: left;
+            font-weight: normal;
+            line-height: 16px;
+        }
+    }
+}
diff --git a/modules/frontend/app/components/ui-grid/template.pug b/modules/frontend/app/components/ui-grid/template.pug
new file mode 100644
index 0000000..4a0f8b3
--- /dev/null
+++ b/modules/frontend/app/components/ui-grid/template.pug
@@ -0,0 +1,60 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+div(ng-if='::$ctrl.gridTreeView')
+    .grid.ui-grid--ignite(
+        ui-grid='$ctrl.grid'
+        ui-grid-resize-columns
+        ui-grid-filters
+        ui-grid-selection
+        ui-grid-exporter
+        ui-grid-pinning
+        ui-grid-tree-view
+        ng-class='{ "ui-grid--thin": $ctrl.gridThin }'
+    )
+
+div(ng-if='::$ctrl.gridGrouping')
+    .grid.ui-grid--ignite(
+        ui-grid='$ctrl.grid'
+        ui-grid-resize-columns
+        ui-grid-filters
+        ui-grid-selection
+        ui-grid-exporter
+        ui-grid-pinning
+        ui-grid-grouping
+        ng-class='{ "ui-grid--thin": $ctrl.gridThin }'
+    )
+
+div(ng-if='::(!$ctrl.gridGrouping && !$ctrl.gridTreeView && $ctrl.onSelectionChange)')
+    .grid.ui-grid--ignite(
+        ui-grid='$ctrl.grid'
+        ui-grid-resize-columns
+        ui-grid-filters
+        ui-grid-selection
+        ui-grid-exporter
+        ui-grid-pinning
+        ng-class='{ "ui-grid--thin": $ctrl.gridThin }'
+    )
+
+div(ng-if='::(!$ctrl.gridGrouping && !$ctrl.gridTreeView && !$ctrl.onSelectionChange)')
+    .grid.ui-grid--ignite(
+        ui-grid='$ctrl.grid'
+        ui-grid-resize-columns
+        ui-grid-filters
+        ui-grid-exporter
+        ui-grid-pinning
+        ng-class='{ "ui-grid--thin": $ctrl.gridThin }'
+    )
diff --git a/modules/frontend/app/components/user-notifications/controller.js b/modules/frontend/app/components/user-notifications/controller.js
new file mode 100644
index 0000000..18199bb
--- /dev/null
+++ b/modules/frontend/app/components/user-notifications/controller.js
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class UserNotificationsController {
+    static $inject = ['deferred', 'message', 'isShown'];
+
+    constructor(deferred, message, isShown) {
+        this.deferred = deferred;
+        this.message = message;
+        this.isShown = isShown;
+    }
+
+    onLoad(editor) {
+        editor.setHighlightActiveLine(false);
+        editor.setAutoScrollEditorIntoView(true);
+        editor.$blockScrolling = Infinity;
+
+        // TODO IGNITE-5366 Ace hangs when it reaches max lines.
+        // const session = editor.getSession();
+        //
+        // session.setUseWrapMode(true);
+        // session.setOption('indentedSoftWrap', false);
+
+        const renderer = editor.renderer;
+
+        renderer.setPadding(7);
+        renderer.setScrollMargin(7, 12);
+        renderer.setHighlightGutterLine(false);
+        renderer.setShowPrintMargin(false);
+        renderer.setShowGutter(false);
+        renderer.setOption('fontFamily', 'monospace');
+        renderer.setOption('fontSize', '14px');
+        renderer.setOption('minLines', '3');
+        renderer.setOption('maxLines', '3');
+
+        editor.focus();
+    }
+
+    submit() {
+        this.deferred.resolve({message: this.message, isShown: this.isShown });
+    }
+}
diff --git a/modules/frontend/app/components/user-notifications/index.js b/modules/frontend/app/components/user-notifications/index.js
new file mode 100644
index 0000000..633890f
--- /dev/null
+++ b/modules/frontend/app/components/user-notifications/index.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import userNotifications from './service';
+
+import './style.scss';
+
+export default angular
+    .module('ignite-console.user-notifications', [])
+    .service('UserNotifications', userNotifications);
diff --git a/modules/frontend/app/components/user-notifications/service.js b/modules/frontend/app/components/user-notifications/service.js
new file mode 100644
index 0000000..5ee3257
--- /dev/null
+++ b/modules/frontend/app/components/user-notifications/service.js
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+import controller from './controller';
+import templateUrl from './template.tpl.pug';
+import {CancellationError} from 'app/errors/CancellationError';
+
+export default class UserNotificationsService {
+    static $inject = ['$http', '$modal', '$q', 'IgniteMessages'];
+
+    /** @type {ng.IQService} */
+    $q;
+
+    /**
+     * @param {ng.IHttpService} $http    
+     * @param {mgcrea.ngStrap.modal.IModalService} $modal   
+     * @param {ng.IQService} $q       
+     * @param {ReturnType<typeof import('app/services/Messages.service').default>} Messages
+     */
+    constructor($http, $modal, $q, Messages) {
+        this.$http = $http;
+        this.$modal = $modal;
+        this.$q = $q;
+        this.Messages = Messages;
+
+        this.message = null;
+        this.isShown = false;
+    }
+
+    set notification(notification) {
+        this.message = _.get(notification, 'message');
+        this.isShown = _.get(notification, 'isShown');
+    }
+
+    editor() {
+        const deferred = this.$q.defer();
+
+        const modal = this.$modal({
+            templateUrl,
+            resolve: {
+                deferred: () => deferred,
+                message: () => this.message,
+                isShown: () => this.isShown
+            },
+            controller,
+            controllerAs: '$ctrl'
+        });
+
+        const modalHide = modal.hide;
+
+        modal.hide = () => deferred.reject(new CancellationError());
+
+        return deferred.promise
+            .finally(modalHide)
+            .then(({ message, isShown }) => {
+                this.$http.put('/api/v1/admin/notifications', { message, isShown })
+                    .catch(this.Messages.showError);
+            });
+    }
+}
diff --git a/modules/frontend/app/components/user-notifications/style.scss b/modules/frontend/app/components/user-notifications/style.scss
new file mode 100644
index 0000000..a1dd94f
--- /dev/null
+++ b/modules/frontend/app/components/user-notifications/style.scss
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../../public/stylesheets/variables';
+
+$disabled-color: #c5c5c5;
+
+#user-notifications-dialog {
+    min-height: 160px;
+}
diff --git a/modules/frontend/app/components/user-notifications/template.tpl.pug b/modules/frontend/app/components/user-notifications/template.tpl.pug
new file mode 100644
index 0000000..a680dea
--- /dev/null
+++ b/modules/frontend/app/components/user-notifications/template.tpl.pug
@@ -0,0 +1,47 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+.modal.modal--ignite.theme--ignite(tabindex='-1' role='dialog')
+    .modal-dialog
+        .modal-content
+            .modal-header
+                h4.modal-title
+                    svg(ignite-icon='gear')
+                    | Set user notifications
+                button.close(type='button' aria-label='Close' ng-click='$hide()')
+                    svg(ignite-icon='cross')
+            .modal-body.modal-body-with-scroll(id='user-notifications-dialog')
+                p
+                    | Enter the text, which will show for all users of the Web Console about an important event or
+                    | warning about ongoing technical works. It will appear #[b on the yellow bar] in the header.
+
+                .form-field__ace.ignite-form-field
+                    +form-field__label({ label: 'Your notification:', name: 'notification', required: true})
+                    .form-field__control
+                        div(ignite-ace='{onLoad: $ctrl.onLoad, mode: "xml"}' ng-trim='true' ng-model='$ctrl.message')
+
+            .modal-footer
+                +form-field__checkbox({
+                    label: 'Show message',
+                    name: 'showMessages',
+                    model: '$ctrl.isShown'
+                })
+
+                div
+                    button.btn-ignite.btn-ignite--link-success(id='btn-cancel' ng-click='$hide()') Cancel
+                    button.btn-ignite.btn-ignite--success(id='btn-submit' ng-click='$ctrl.submit()') Submit
diff --git a/modules/frontend/app/components/version-picker/component.js b/modules/frontend/app/components/version-picker/component.js
new file mode 100644
index 0000000..16fcb7e
--- /dev/null
+++ b/modules/frontend/app/components/version-picker/component.js
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+import template from './template.pug';
+import './style.scss';
+
+export default {
+    template,
+    controller: class {
+        static $inject = ['IgniteVersion', '$scope'];
+
+        /**
+         * @param {import('app/services/Version.service').default} Version
+         * @param {ng.IRootScopeService} $scope
+         */
+        constructor(Version, $scope) {
+            this.currentSbj = Version.currentSbj;
+            this.supportedVersions = Version.supportedVersions;
+
+            const dropdownToggle = (active) => {
+                this.isActive = active;
+
+                // bs-dropdown does not call apply on callbacks
+                $scope.$apply();
+            };
+
+            this.onDropdownShow = () => dropdownToggle(true);
+            this.onDropdownHide = () => dropdownToggle(false);
+        }
+
+        $onInit() {
+            this.dropdown = _.map(this.supportedVersions, (ver) => ({
+                text: ver.label,
+                click: () => this.currentSbj.next(ver)
+            }));
+
+            this.currentSbj.subscribe({
+                next: (ver) => this.currentVersion = ver.label
+            });
+        }
+    }
+};
diff --git a/modules/frontend/app/components/version-picker/index.js b/modules/frontend/app/components/version-picker/index.js
new file mode 100644
index 0000000..04ff903
--- /dev/null
+++ b/modules/frontend/app/components/version-picker/index.js
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('ignite-console.version-picker', [
+        'ignite-console.services'
+    ])
+    .component('versionPicker', component);
diff --git a/modules/frontend/app/components/version-picker/style.scss b/modules/frontend/app/components/version-picker/style.scss
new file mode 100644
index 0000000..6d962c2
--- /dev/null
+++ b/modules/frontend/app/components/version-picker/style.scss
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+version-picker {
+    display: inline-flex;
+    align-items: center;
+
+    .btn-ignite {
+        border-radius: 9px;
+        min-height: 0;
+        font-size: 12px;
+        font-weight: bold;
+        line-height: 17px;
+        padding-top: 0;
+        padding-bottom: 1px;
+    }
+
+    [ignite-icon] {
+        margin-left: 5px;
+    }
+
+    .dropdown-menu a {
+        // Fixes style leak from above
+        font-size: 14px !important;
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/components/version-picker/template.pug b/modules/frontend/app/components/version-picker/template.pug
new file mode 100644
index 0000000..5d35b78
--- /dev/null
+++ b/modules/frontend/app/components/version-picker/template.pug
@@ -0,0 +1,37 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.btn-ignite.btn-ignite--primary(
+    bs-dropdown='$ctrl.dropdown'
+    data-trigger='hover focus'
+    bs-on-before-show='$ctrl.onDropdownShow'
+    bs-on-before-hide='$ctrl.onDropdownHide'
+    data-container='self'
+    ng-class='{"active": $ctrl.isActive }'
+)
+    | {{$ctrl.currentVersion}}
+    span.icon-right.fa.fa-caret-down
+
+svg.icon-help(
+    ignite-icon='info'
+    bs-tooltip=''
+    data-title=`
+        Web Console supports multiple Ignite versions.
+        <br>
+        Select version you need to configure cluster.
+    `
+    data-placement='right'
+)
\ No newline at end of file
diff --git a/modules/frontend/app/components/web-console-footer/component.js b/modules/frontend/app/components/web-console-footer/component.js
new file mode 100644
index 0000000..bb2f7f7
--- /dev/null
+++ b/modules/frontend/app/components/web-console-footer/component.js
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+import controller from './controller';
+
+export default {
+    template,
+    controller
+};
diff --git a/modules/frontend/app/components/web-console-footer/controller.ts b/modules/frontend/app/components/web-console-footer/controller.ts
new file mode 100644
index 0000000..0ca604c
--- /dev/null
+++ b/modules/frontend/app/components/web-console-footer/controller.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {default as Version} from '../../services/Version.service';
+
+export default class WebConsoleFooter {
+    static $inject = ['IgniteVersion', '$rootScope'];
+
+    constructor(private Version: Version, private $root: ng.IRootScopeService) {}
+
+    year = new Date().getFullYear();
+
+    get userIsAuthorized() {
+        return !!this.$root.user;
+    }
+}
diff --git a/modules/frontend/app/components/web-console-footer/index.js b/modules/frontend/app/components/web-console-footer/index.js
new file mode 100644
index 0000000..4fa4617
--- /dev/null
+++ b/modules/frontend/app/components/web-console-footer/index.js
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('ignite-console.web-console-footer', [])
+    .component('webConsoleFooter', component)
+    .directive('webConsoleFooterPageBottom', function() {
+        return {
+            restrict: 'C',
+            link(scope, el) {
+                el.parent().addClass('wrapper-public');
+                scope.$on('$destroy', () => el.parent().removeClass('wrapper-public'));
+            }
+        };
+    });
diff --git a/modules/frontend/app/components/web-console-footer/style.scss b/modules/frontend/app/components/web-console-footer/style.scss
new file mode 100644
index 0000000..fe5eb64
--- /dev/null
+++ b/modules/frontend/app/components/web-console-footer/style.scss
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+web-console-footer {
+    font-size: 12px;
+
+    .#{&}__legal {
+        color: rgba(0, 0, 0, 0.54);
+    }
+
+    &.web-console-footer__sidebar-closed {
+        width: var(--width-narrow);
+        border-top: 1px solid #dddddd;
+    
+        a {
+            color: var(--inactive-link-color);
+            margin-left: auto;
+            margin-right: auto;
+            width: 40px;
+            height: 40px;
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+
+            &:hover, &:focus {
+                color: var(--active-link-color);
+            }
+            [ignite-icon] {
+                transform: scale(1.25);
+            }
+        }
+    
+        .web-console-footer__sidebar-stuff {
+            position: sticky;
+            display: grid;
+            grid-row-gap: 15px;
+            padding-top: 20px;
+            padding-bottom: 20px;
+        }
+    
+        .web-console-footer__regular-stuff {
+            display: none;
+        }
+    }
+
+    &.web-console-footer__sidebar-opened {
+        width: var(--width-wide);
+        display: flex;
+        flex-direction: column;
+        padding: 27px 18px 27px 27px;
+
+        .web-console-footer__sidebar-stuff {
+            display: none;
+        }        
+    
+        .web-console-footer__regular-stuff {
+            display: block;
+        }
+    }
+}
+
+.web-console-footer__page-bottom {
+    grid-area: footer;
+    color: #fafafa;
+    background: #393939;
+    font-size: 12px;
+    font-weight: 300;
+    padding: 12px var(--page-side-padding);
+
+    .web-console-footer__sidebar-stuff {
+        display: none;
+    }
+
+    p {
+        margin: 0;
+    }
+
+    a {
+        color: #ee8e89 !important;
+        &:hover, &:focus {
+            color: #ee2b27 !important;
+        }
+    }
+
+    .web-console-footer__legal {
+        color: inherit;
+    }    
+}
diff --git a/modules/frontend/app/components/web-console-footer/template.pug b/modules/frontend/app/components/web-console-footer/template.pug
new file mode 100644
index 0000000..2c1495e
--- /dev/null
+++ b/modules/frontend/app/components/web-console-footer/template.pug
@@ -0,0 +1,32 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+
+.web-console-footer__sidebar-stuff
+    a(
+        href="/api/v1/downloads/agent"
+        target="_self"
+        ng-if='$ctrl.userIsAuthorized'
+        title='Download Agent'
+    )
+        svg(ignite-icon='downloadAgent')
+
+.web-console-footer__regular-stuff
+    p Apache Ignite Web Console ({{$ctrl.Version.webConsole}})
+    p © {{::$ctrl.year}} The Apache Software Foundation.
+    p.web-console-footer__legal Apache, Apache Ignite, the Apache feather and the Apache Ignite logo are trademarks of The Apache Software Foundation.
+
+    a(href="/api/v1/downloads/agent" target="_self" ng-if='$ctrl.userIsAuthorized') Download Agent
\ No newline at end of file
diff --git a/modules/frontend/app/components/web-console-header/component.ts b/modules/frontend/app/components/web-console-header/component.ts
new file mode 100644
index 0000000..ad7e8a7
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/component.ts
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+import {AppStore, toggleSidebar} from '../../store';
+
+export default {
+    template,
+    controller: class {
+        static $inject = ['$rootScope', '$scope', '$state', 'IgniteBranding', 'UserNotifications', 'Store'];
+
+        constructor($rootScope, $scope, $state, branding, UserNotifications, private store: AppStore) {
+            Object.assign(this, {$rootScope, $scope, $state, branding, UserNotifications});
+        }
+
+        toggleSidebar() {
+            this.store.dispatch(toggleSidebar());
+        }
+
+        isAuthorized() {
+            return !!this.$rootScope.user;
+        }
+    },
+    transclude: true,
+    bindings: {
+        hideMenuButton: '<?'
+    }
+};
diff --git a/modules/frontend/app/components/web-console-header/components/demo-mode-button/component.ts b/modules/frontend/app/components/web-console-header/components/demo-mode-button/component.ts
new file mode 100644
index 0000000..e24b551
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/components/demo-mode-button/component.ts
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+
+export const component: ng.IComponentOptions = {
+    template,
+    controller
+};
diff --git a/modules/frontend/app/components/web-console-header/components/demo-mode-button/controller.ts b/modules/frontend/app/components/web-console-header/components/demo-mode-button/controller.ts
new file mode 100644
index 0000000..23280d9
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/components/demo-mode-button/controller.ts
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {StateService} from '@uirouter/angularjs';
+import {default as LegacyConfirmFactory} from 'app/services/Confirm.service';
+
+export default class DemoModeButton {
+    static $inject = ['$rootScope', '$state', '$window', 'IgniteConfirm', 'AgentManager', 'IgniteMessages'];
+
+    constructor(
+        private $root: ng.IRootScopeService,
+        private $state: StateService,
+        private $window: ng.IWindowService,
+        private Confirm: ReturnType<typeof LegacyConfirmFactory>,
+        private agentMgr: AgentManager,
+        private Messages
+    ) {}
+
+    private _openTab(stateName: string) {
+        this.$window.open(this.$state.href(stateName, {}), '_blank');
+    }
+
+    startDemo() {
+        const connectionState = this.agentMgr.connectionSbj.getValue();
+        const disconnected = _.get(connectionState, 'state') === 'AGENT_DISCONNECTED';
+        const demoEnabled = _.get(connectionState, 'hasDemo');
+
+        if (disconnected || demoEnabled || _.isNil(demoEnabled)) {
+            if (!this.$root.user.demoCreated)
+                return this._openTab('demo.reset');
+
+            this.Confirm.confirm('Would you like to continue with previous demo session?', true, false)
+                .then((resume) => {
+                    if (resume)
+                        return this._openTab('demo.resume');
+
+                    this._openTab('demo.reset');
+                });
+        }
+        else
+            this.Messages.showError('Demo mode disabled by administrator');
+    }
+}
diff --git a/modules/frontend/app/components/web-console-header/components/demo-mode-button/template.pug b/modules/frontend/app/components/web-console-header/components/demo-mode-button/template.pug
new file mode 100644
index 0000000..f566ac4
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/components/demo-mode-button/template.pug
@@ -0,0 +1,20 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+
+button.btn-ignite.btn-ignite--success(
+    ng-click='$ctrl.startDemo()'
+) Start Demo
\ No newline at end of file
diff --git a/modules/frontend/app/components/web-console-header/components/user-menu/component.ts b/modules/frontend/app/components/web-console-header/components/user-menu/component.ts
new file mode 100644
index 0000000..f6141d9
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/components/user-menu/component.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+import './style.scss';
+
+export default {
+    template,
+    controller
+};
diff --git a/modules/frontend/app/components/web-console-header/components/user-menu/controller.ts b/modules/frontend/app/components/web-console-header/components/user-menu/controller.ts
new file mode 100644
index 0000000..dc5768a
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/components/user-menu/controller.ts
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class UserMenu {
+    static $inject = ['$rootScope', 'IgniteUserbar', 'AclService', '$state', 'gettingStarted'];
+
+    constructor(
+        private $root: ng.IRootScopeService,
+        private IgniteUserbar: any,
+        private AclService: any,
+        private $state: any,
+        private gettingStarted: any
+    ) {}
+
+    $onInit() {
+        this.items = [
+            {text: 'Profile', sref: 'base.settings.profile'},
+            {text: 'Getting started', click: '$ctrl.gettingStarted.tryShow(true)'}
+        ];
+
+        const _rebuildSettings = () => {
+            this.items.splice(2);
+
+            if (this.AclService.can('admin_page'))
+                this.items.push({text: 'Admin panel', sref: 'base.settings.admin'});
+
+            this.items.push(...this.IgniteUserbar);
+
+            if (this.AclService.can('logout'))
+                this.items.push({text: 'Log out', sref: 'logout'});
+        };
+
+        if (this.$root.user)
+            _rebuildSettings(null, this.$root.user);
+
+        this.$root.$on('user', _rebuildSettings);
+    }
+
+    get user() {
+        return this.$root.user;
+    }
+}
diff --git a/modules/frontend/app/components/web-console-header/components/user-menu/style.scss b/modules/frontend/app/components/web-console-header/components/user-menu/style.scss
new file mode 100644
index 0000000..9d9e3e3
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/components/user-menu/style.scss
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+user-menu {
+    --active-link-color: #ee2b27;
+    font-size: 14px;
+    max-width: 150px;
+    max-height: 100%;
+    display: flex;
+
+    &>.active {
+        color: var(--active-link-color)
+    }
+    .caret {
+        margin-left: 8px;
+        margin-top: 3px;
+        align-self: center;
+    }
+    .user-menu__username {
+        text-overflow: ellipsis;
+        overflow: hidden;
+        white-space: nowrap;
+    }
+    .user-menu__trigger {
+        display: flex;
+        max-height: 100%;
+        overflow: hidden;
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/components/web-console-header/components/user-menu/template.pug b/modules/frontend/app/components/web-console-header/components/user-menu/template.pug
new file mode 100644
index 0000000..4940e52
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/components/user-menu/template.pug
@@ -0,0 +1,26 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+div.user-menu__trigger(
+    ng-class='{active: $ctrl.$state.includes("base.settings")}'
+    ng-click='$event.stopPropagation()'
+    bs-dropdown='$ctrl.items'
+    data-placement='bottom-right'
+    data-trigger='hover focus'
+    data-container='self'
+)
+    span.user-menu__username {{$ctrl.user.firstName}} {{$ctrl.user.lastName}}
+    span.caret
\ No newline at end of file
diff --git a/modules/frontend/app/components/web-console-header/components/web-console-header-content/component.ts b/modules/frontend/app/components/web-console-header/components/web-console-header-content/component.ts
new file mode 100644
index 0000000..e86d3e5
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/components/web-console-header-content/component.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+import './style.scss';
+
+export const component: ng.IComponentOptions = {
+    template,
+    controller
+};
diff --git a/modules/frontend/app/components/web-console-header/components/web-console-header-content/controller.ts b/modules/frontend/app/components/web-console-header/components/web-console-header-content/controller.ts
new file mode 100644
index 0000000..2ae9000
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/components/web-console-header-content/controller.ts
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {StateService} from '@uirouter/angularjs';
+
+export default class WebConsoleHeaderContent {
+    static $inject = ['$rootScope', '$state'];
+
+    constructor(
+        private $rootScope: ng.IRootScopeService,
+        private $state: StateService
+    ) {}
+
+    static connectedClusterInvisibleStates = [
+        '403', '404', 'signin'
+    ];
+
+    get showConnectedClusters(): boolean {
+        return this.$rootScope.user &&
+            !this.$rootScope.IgniteDemoMode &&
+            !this.constructor.connectedClusterInvisibleStates.some((state) => this.$state.includes(state)) &&
+            !this.$rootScope.user.becomeUsed;
+    }
+
+    get showUserMenu(): boolean {
+        return !!this.$rootScope.user;
+    }
+
+    get showDemoModeButton(): boolean {
+        return this.$rootScope.user && !this.$rootScope.user.becomeUsed && !this.$rootScope.IgniteDemoMode;
+    }
+}
diff --git a/modules/frontend/app/components/web-console-header/components/web-console-header-content/style.scss b/modules/frontend/app/components/web-console-header/components/web-console-header-content/style.scss
new file mode 100644
index 0000000..f483614
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/components/web-console-header-content/style.scss
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+web-console-header-content {
+    display: flex;
+    flex: 1;
+
+    .web-console-header-content__right {
+        margin-left: auto;
+        display: flex;
+    }
+
+    .web-console-header-item {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        justify-content: center;
+        padding: 0 20px;
+        border-left: 1px solid #dddddd;
+        height: 100%;
+    }
+}
+
+.web-console-header-content__title {
+    display: flex;
+    align-items: center;
+    margin-left: 80px;
+    font-size: 1.4em;
+    margin-right: auto;
+}
\ No newline at end of file
diff --git a/modules/frontend/app/components/web-console-header/components/web-console-header-content/template.pug b/modules/frontend/app/components/web-console-header/components/web-console-header-content/template.pug
new file mode 100644
index 0000000..8f1d4f8
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/components/web-console-header-content/template.pug
@@ -0,0 +1,24 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.web-console-header-content__right
+    .web-console-header-item(ng-if='$ctrl.showConnectedClusters')
+        connected-clusters
+    .web-console-header-item(ng-if='$ctrl.showDemoModeButton')
+        demo-mode-button
+
+.web-console-header-item(ng-if='$ctrl.showUserMenu')
+    user-menu
\ No newline at end of file
diff --git a/modules/frontend/app/components/web-console-header/index.js b/modules/frontend/app/components/web-console-header/index.js
new file mode 100644
index 0000000..2a106d4
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/index.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+import userMenu from './components/user-menu/component';
+import {component as content} from './components/web-console-header-content/component';
+import {component as demo} from './components/demo-mode-button/component';
+
+export default angular
+    .module('ignite-console.web-console-header', [])
+    .component('webConsoleHeader', component)
+    .component('webConsoleHeaderContent', content)
+    .component('userMenu', userMenu)
+    .component('demoModeButton', demo);
diff --git a/modules/frontend/app/components/web-console-header/style.scss b/modules/frontend/app/components/web-console-header/style.scss
new file mode 100644
index 0000000..44baea4
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/style.scss
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+web-console-header {
+    @import "./../../../public/stylesheets/variables.scss";
+
+    $nav-item-margin: 30px;
+    $bottom-border-width: 4px;
+
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    height: 62px;
+    font-size: 16px;
+    border-bottom: $bottom-border-width solid red;
+    background: white;
+    position: relative;
+    padding-left: 16px;
+
+    &>ng-transclude {
+        display: flex;
+        flex-direction: row;
+        justify-content: flex-end;
+        height: 100%;
+        flex: 1;
+    }
+
+    &:after {
+        // Shows header shadow over absolutely positioned child content,
+        // otherwise z ordering issues happen.
+        display: block;
+        width: 100%;
+        content: '';
+        height: 20px;
+        position: absolute;
+        overflow: hidden;
+        bottom: -4px;
+        pointer-events: none;
+    }
+
+    .wch-slot {
+        &>* {
+            display: flex;
+            flex-direction: row;
+            justify-content: center;
+            align-items: center;
+            padding-top: 7px;
+        }
+
+        &.wch-slot-left {
+            margin-left: 80px;
+        }
+
+        &.wch-slot-right {
+            margin: 0 0 0 auto;
+        }
+    }
+
+    .wch-logo {
+        height: 40px;
+    }
+
+    .wch-additional-nav-items {
+        display: flex;
+    }
+
+    .web-console-header__togle-menu-button {
+        width: 40px;
+        height: 40px;
+        background: none !important;
+        border: none !important;
+        margin: 0 !important;
+        padding: 0 !important;
+        outline: none !important;
+        color: rgba(0, 0, 0, 0.54);
+        margin-right: 5px;
+    }
+    .web-console-header__togle-menu-button-hidden {
+        visibility: hidden;
+    }
+}
diff --git a/modules/frontend/app/components/web-console-header/template.pug b/modules/frontend/app/components/web-console-header/template.pug
new file mode 100644
index 0000000..940f3a4
--- /dev/null
+++ b/modules/frontend/app/components/web-console-header/template.pug
@@ -0,0 +1,28 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+button.web-console-header__togle-menu-button(
+    type='button'
+    title='Toggle menu'
+    ng-click='$ctrl.toggleSidebar()'
+    ng-class='::{"web-console-header__togle-menu-button-hidden": $ctrl.hideMenuButton}'
+)
+    svg(ignite-icon='menu')
+
+a(ui-sref='{{$ctrl.isAuthorized() ? "default-state" : "landing"}}').wch-logo-anchor
+    img.wch-logo(src='/images/ignite-logo.svg')
+
+ng-transclude
diff --git a/modules/frontend/app/components/web-console-sidebar/component.ts b/modules/frontend/app/components/web-console-sidebar/component.ts
new file mode 100644
index 0000000..e86d3e5
--- /dev/null
+++ b/modules/frontend/app/components/web-console-sidebar/component.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+import './style.scss';
+
+export const component: ng.IComponentOptions = {
+    template,
+    controller
+};
diff --git a/modules/frontend/app/components/web-console-sidebar/controller.ts b/modules/frontend/app/components/web-console-sidebar/controller.ts
new file mode 100644
index 0000000..02b21a1
--- /dev/null
+++ b/modules/frontend/app/components/web-console-sidebar/controller.ts
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {AppStore, selectSidebarOpened} from '../../store';
+
+export default class WebConsoleSidebar {
+    static $inject = ['$rootScope', 'Store'];
+
+    constructor(
+        private $rootScope: ng.IRootScopeService,
+        private store: AppStore
+    ) {}
+
+    sidebarOpened$ = this.store.state$.pipe(selectSidebarOpened());
+
+    get showNavigation(): boolean {
+        return !!this.$rootScope.user;
+    }
+}
diff --git a/modules/frontend/app/components/web-console-sidebar/index.ts b/modules/frontend/app/components/web-console-sidebar/index.ts
new file mode 100644
index 0000000..717dca5
--- /dev/null
+++ b/modules/frontend/app/components/web-console-sidebar/index.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {component as overflow} from './web-console-sidebar-overflow/component';
+import {component as nav} from './web-console-sidebar-navigation/component';
+import {component as sidebar} from './component';
+
+export default angular.module('sidebar', [])
+    .component('webConsoleSidebarOverflow', overflow)
+    .component('webConsoleSidebarNavigation', nav)
+    .component('webConsoleSidebar', sidebar);
diff --git a/modules/frontend/app/components/web-console-sidebar/style.scss b/modules/frontend/app/components/web-console-sidebar/style.scss
new file mode 100644
index 0000000..b625333
--- /dev/null
+++ b/modules/frontend/app/components/web-console-sidebar/style.scss
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+web-console-sidebar {
+    display: flex;
+    flex-direction: column;
+    background: white;
+    border-right: 1px #dddddd solid;
+    --width-narrow: 75px;
+    --width-wide: 280px;
+    --active-link-color: #ee2b27;
+    --inactive-link-color: #757575;
+    // Does not include notifications height
+    max-height: calc(100vh - var(--header-height));
+    position: -webkit-sticky;
+    position: sticky;
+    top: var(--header-height);
+
+    web-console-sidebar-overflow {
+        flex: 1;
+    }
+
+    web-console-footer {
+        flex: 0 0 auto;
+
+        &:first-child {
+            margin-top: auto;
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/components/web-console-sidebar/template.pug b/modules/frontend/app/components/web-console-sidebar/template.pug
new file mode 100644
index 0000000..f02f2c4
--- /dev/null
+++ b/modules/frontend/app/components/web-console-sidebar/template.pug
@@ -0,0 +1,22 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+web-console-sidebar-overflow
+    web-console-sidebar-navigation(opened='$ctrl.sidebarOpened$|async:this' ng-if='$ctrl.showNavigation')
+web-console-footer(ng-class=`{
+    "web-console-footer__sidebar-opened": ($ctrl.sidebarOpened$|async:this),
+    "web-console-footer__sidebar-closed": !($ctrl.sidebarOpened$|async:this)
+}`)
\ No newline at end of file
diff --git a/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-navigation/component.ts b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-navigation/component.ts
new file mode 100644
index 0000000..4e4647f
--- /dev/null
+++ b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-navigation/component.ts
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './style.scss';
+import template from './template.pug';
+import controller from './controller';
+
+export const component = {
+    controller,
+    template,
+    bindings: {
+        opened: '<'
+    }
+};
diff --git a/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-navigation/controller.ts b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-navigation/controller.ts
new file mode 100644
index 0000000..3daafe8
--- /dev/null
+++ b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-navigation/controller.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {AppStore, selectNavigationMenu} from '../../../store';
+
+export default class WebConsoleSidebarNavigation {
+    static $inject = ['Store'];
+
+    constructor(private store: AppStore) {}
+
+    menu$ = this.store.state$.pipe(selectNavigationMenu());
+}
diff --git a/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-navigation/style.scss b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-navigation/style.scss
new file mode 100644
index 0000000..1b2c4ff
--- /dev/null
+++ b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-navigation/style.scss
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+web-console-sidebar-navigation {
+    display: flex;
+    flex-direction: column;
+    padding-top: 14px;
+
+    nav {
+        display: flex;
+        flex-direction: column;
+        list-style-type: none;
+
+        &>li {
+            display: flex;
+        }
+    }
+
+    .web-console-sidebar-navigation__link-icon {
+        width: 20px;
+        height: auto;
+    }
+
+    .web-console-sidebar-navigation__link-narrow {
+        height: var(--width-narrow);
+        width: var(--width-narrow);
+        color: var(--inactive-link-color);
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+        &:hover, &:focus {
+            color: var(--active-link-color);
+
+        }
+
+        &.web-console-sidebar-navigation__link-active {
+            color: var(--active-link-color);
+            position: relative;
+
+            &:after {
+                content: '';
+                display: block;
+                width: 2px;
+                // Line can't be outside of scrollable container
+                right: 0;
+                top: 0;
+                bottom: 0;
+                position: absolute;
+                background: var(--active-link-color);
+            }
+        }
+        .web-console-sidebar-navigation__link-content {
+            display: none;
+        }
+    }
+
+    li:first-of-type > .web-console-sidebar-navigation__link-wide {
+        margin-top: 18px;        
+    }
+
+    .web-console-sidebar-navigation__link-wide {
+        color: #393939;
+        text-decoration: none !important;
+        line-height: 19px;
+        font-size: 16px;
+        padding: 10px 28px 11px;
+        width: var(--width-wide);
+        display: inline-flex;
+        align-items: center;
+
+        &:hover, &:focus {
+            color: var(--active-link-color);
+
+            .web-console-sidebar-navigation__link-icon {
+                color: inherit;
+            }
+        }
+        &.web-console-sidebar-navigation__link-active {
+            color: white;
+            background: var(--active-link-color);
+
+            .web-console-sidebar-navigation__link-icon {
+                color: inherit;
+            }
+        }
+        .web-console-sidebar-navigation__link-icon {
+            margin-right: 12px;
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-navigation/template.pug b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-navigation/template.pug
new file mode 100644
index 0000000..c35de9d
--- /dev/null
+++ b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-navigation/template.pug
@@ -0,0 +1,35 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+nav
+    li(
+        ng-repeat='item in $ctrl.menu$|async:this track by item.sref'
+        ng-if='!item.hidden'
+    )
+        a.web-console-sidebar-navigation__link(
+            ui-sref='{{item.sref}}'
+            ui-sref-active=`{
+                "web-console-sidebar-navigation__link-active": item.activeSref
+            }`
+            title='{{item.label|translate}}'
+            ng-class=`{
+                'web-console-sidebar-navigation__link-narrow': !$ctrl.opened,
+                'web-console-sidebar-navigation__link-wide': $ctrl.opened
+            }`
+            tabindex='0'
+        )
+            svg.web-console-sidebar-navigation__link-icon(ignite-icon='{{item.icon}}')
+            span.web-console-sidebar-navigation__link-content(translate='{{item.label}}')
diff --git a/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-overflow/component.ts b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-overflow/component.ts
new file mode 100644
index 0000000..0f69e2d
--- /dev/null
+++ b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-overflow/component.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import controller from './controller';
+import template from './template.pug';
+import './style.scss';
+
+export const component: ng.IComponentOptions =  {
+    controller,
+    transclude: true,
+    template
+};
diff --git a/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-overflow/controller.ts b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-overflow/controller.ts
new file mode 100644
index 0000000..6c2ebb9
--- /dev/null
+++ b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-overflow/controller.ts
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import ResizeObserver from 'resize-observer-polyfill';
+
+export default class WebCOnsoleSidebarOverflow {
+    static $inject = ['$element', 'gridUtil', '$window'];
+
+    constructor(private el: JQLite, private gridUtil: {getScrollbarWidth(): number}, private $win: ng.IWindowService) {}
+
+    scrollEl!: JQLite;
+
+    resizeObserver: ResizeObserver;
+
+    $onInit() {
+        this.el.css('--scrollbar-width', this.gridUtil.getScrollbarWidth());
+    }
+
+    $postLink() {
+        this.scrollEl[0].addEventListener('scroll', this.onScroll, {passive: true});
+        this.resizeObserver = new ResizeObserver(() => this.applyStyles(this.scrollEl[0]));
+        this.resizeObserver.observe(this.el[0]);
+    }
+    $onDestroy() {
+        this.scrollEl[0].removeEventListener('scroll', this.onScroll);
+        this.resizeObserver.disconnect();
+    }
+    applyStyles(target: HTMLElement) {
+        const {offsetHeight, scrollTop, scrollHeight} = target;
+        const top = scrollTop !== 0;
+        const bottom = Math.floor((offsetHeight + scrollTop)) !== Math.floor(scrollHeight);
+
+        target.classList.toggle('top', top);
+        target.classList.toggle('bottom', bottom);
+    }
+    onScroll = (e: UIEvent) => {
+        this.$win.requestAnimationFrame(() => {
+            this.applyStyles(e.target as HTMLElement);
+        });
+    }
+}
diff --git a/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-overflow/style.scss b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-overflow/style.scss
new file mode 100644
index 0000000..cbc6adc
--- /dev/null
+++ b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-overflow/style.scss
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+web-console-sidebar-overflow {
+    overflow: hidden;
+    position: relative;
+    display: flex;
+
+    &>.web-console-sidebar-overflow__scroll-root {
+        overflow-x: hidden;
+        overflow-y: scroll;
+        margin-right: calc(-1 * var(--scrollbar-width));
+
+        &.top:before {
+            display: block;
+            width: 100%;
+            content: '';
+            height: 1px;
+            position: absolute;
+            top: -1px;
+            box-shadow: 0 0 9px 1px #a0a0a0;
+            transform: translateZ(1px);
+        }
+        &.bottom:after {
+            display: block;
+            width: 100%;
+            content: '';
+            position: absolute;
+            bottom: 0;
+            box-shadow: 0 0 9px 1px #a0a0a0;
+            transform: translateZ(1px);
+        }
+
+        &>ng-transclude {
+            display: block;
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-overflow/template.pug b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-overflow/template.pug
new file mode 100644
index 0000000..5b70da6
--- /dev/null
+++ b/modules/frontend/app/components/web-console-sidebar/web-console-sidebar-overflow/template.pug
@@ -0,0 +1,18 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.web-console-sidebar-overflow__scroll-root(ng-ref='$ctrl.scrollEl' ng-ref-read='$element')
+    ng-transclude(ng-style='::{"--scrollbar-width": $ctrl.SCROLLBAR_WIDTH}')
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/button-download-project/component.ts b/modules/frontend/app/configuration/components/button-download-project/component.ts
new file mode 100644
index 0000000..0aeaf8f
--- /dev/null
+++ b/modules/frontend/app/configuration/components/button-download-project/component.ts
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import ConfigurationDownload from '../../services/ConfigurationDownload';
+
+export class ButtonDownloadProject {
+    static $inject = ['ConfigurationDownload'];
+
+    constructor(private ConfigurationDownload: ConfigurationDownload) {}
+
+    cluster: any;
+
+    download() {
+        return this.ConfigurationDownload.downloadClusterConfiguration(this.cluster);
+    }
+}
+export const component = {
+    name: 'buttonDownloadProject',
+    controller: ButtonDownloadProject,
+    template,
+    bindings: {
+        cluster: '<'
+    }
+};
diff --git a/modules/frontend/app/configuration/components/button-download-project/index.ts b/modules/frontend/app/configuration/components/button-download-project/index.ts
new file mode 100644
index 0000000..4a220db
--- /dev/null
+++ b/modules/frontend/app/configuration/components/button-download-project/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import {component} from './component';
+
+export default angular
+.module('configuration.button-download-project', [])
+.component(component.name, component);
diff --git a/modules/frontend/app/configuration/components/button-download-project/template.pug b/modules/frontend/app/configuration/components/button-download-project/template.pug
new file mode 100644
index 0000000..0264676
--- /dev/null
+++ b/modules/frontend/app/configuration/components/button-download-project/template.pug
@@ -0,0 +1,22 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+button.btn-ignite.btn-ignite--success(
+    type='button'
+    ng-click='$ctrl.download()'
+)
+    svg(ignite-icon='download').icon-left
+    | Download project
diff --git a/modules/frontend/app/configuration/components/button-import-models/component.ts b/modules/frontend/app/configuration/components/button-import-models/component.ts
new file mode 100644
index 0000000..0127468
--- /dev/null
+++ b/modules/frontend/app/configuration/components/button-import-models/component.ts
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+import ModalImportModels from '../modal-import-models/service';
+
+export class ButtonImportModels {
+    static $inject = ['ModalImportModels'];
+
+    constructor(private ModalImportModels: ModalImportModels) {}
+
+    clusterId: string;
+
+    startImport() {
+        return this.ModalImportModels.open();
+    }
+}
+export const component = {
+    name: 'buttonImportModels',
+    controller: ButtonImportModels,
+    template,
+    bindings: {
+        clusterID: '<clusterId'
+    }
+};
diff --git a/modules/frontend/app/configuration/components/button-import-models/index.ts b/modules/frontend/app/configuration/components/button-import-models/index.ts
new file mode 100644
index 0000000..352f8a7
--- /dev/null
+++ b/modules/frontend/app/configuration/components/button-import-models/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import {component} from './component';
+
+export default angular
+    .module('configuration.button-import-models', [])
+    .component(component.name, component);
diff --git a/modules/frontend/app/configuration/components/button-import-models/style.scss b/modules/frontend/app/configuration/components/button-import-models/style.scss
new file mode 100644
index 0000000..4944626
--- /dev/null
+++ b/modules/frontend/app/configuration/components/button-import-models/style.scss
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+button-import-models {
+    display: inline-block;
+
+    button {
+        // Ensures same height for wrapper element and button
+        vertical-align: top;
+    }
+}
diff --git a/modules/frontend/app/configuration/components/button-import-models/template.pug b/modules/frontend/app/configuration/components/button-import-models/template.pug
new file mode 100644
index 0000000..25c3531
--- /dev/null
+++ b/modules/frontend/app/configuration/components/button-import-models/template.pug
@@ -0,0 +1,20 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+button.btn-ignite.btn-ignite--primary(
+    ng-click='$ctrl.startImport()'
+    type='button'
+) Import from Database
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/button-preview-project/component.ts b/modules/frontend/app/configuration/components/button-preview-project/component.ts
new file mode 100644
index 0000000..d0b50f0
--- /dev/null
+++ b/modules/frontend/app/configuration/components/button-preview-project/component.ts
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import ModalPreviewProject from '../modal-preview-project/service';
+
+export class ButtonPreviewProject {
+    static $inject = ['ModalPreviewProject'];
+
+    constructor(private ModalPreviewProject: ModalPreviewProject) {}
+
+    cluster: any;
+
+    preview() {
+        return this.ModalPreviewProject.open(this.cluster);
+    }
+}
+export const component = {
+    name: 'buttonPreviewProject',
+    controller: ButtonPreviewProject,
+    template,
+    bindings: {
+        cluster: '<'
+    }
+};
diff --git a/modules/frontend/app/configuration/components/button-preview-project/index.ts b/modules/frontend/app/configuration/components/button-preview-project/index.ts
new file mode 100644
index 0000000..31f76c9
--- /dev/null
+++ b/modules/frontend/app/configuration/components/button-preview-project/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import {component} from './component';
+
+export default angular
+    .module('configuration.button-preview-project', [])
+    .component(component.name, component);
diff --git a/modules/frontend/app/configuration/components/button-preview-project/template.pug b/modules/frontend/app/configuration/components/button-preview-project/template.pug
new file mode 100644
index 0000000..3c3ca38
--- /dev/null
+++ b/modules/frontend/app/configuration/components/button-preview-project/template.pug
@@ -0,0 +1,22 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+button.btn-ignite.btn-ignite--link-dashed-success(
+    type='button'
+    ng-click='$ctrl.preview()'
+)
+    svg(ignite-icon='structure').icon-left
+    | See project structure
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/fakeUICanExit.spec.js b/modules/frontend/app/configuration/components/fakeUICanExit.spec.js
new file mode 100644
index 0000000..7d21eba
--- /dev/null
+++ b/modules/frontend/app/configuration/components/fakeUICanExit.spec.js
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {spy} from 'sinon';
+import {assert} from 'chai';
+import {FakeUiCanExitController} from './fakeUICanExit';
+
+suite('Page configuration fakeUIcanExit directive', () => {
+    test('It unsubscribes from state events when destroyed', () => {
+        const $element = {data: () => [{uiCanExit: () => {}}]};
+        const off = spy();
+        const $transitions = {onBefore: () => off};
+        const i = new FakeUiCanExitController($element, $transitions);
+        i.$onInit();
+        i.$onDestroy();
+        assert.ok(off.calledOnce, 'Calls off when destroyed');
+    });
+});
diff --git a/modules/frontend/app/configuration/components/fakeUICanExit.ts b/modules/frontend/app/configuration/components/fakeUICanExit.ts
new file mode 100644
index 0000000..02fb216
--- /dev/null
+++ b/modules/frontend/app/configuration/components/fakeUICanExit.ts
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TransitionService} from '@uirouter/angularjs';
+
+export class FakeUiCanExitController {
+    static $inject = ['$element', '$transitions'];
+    static CALLBACK_NAME = 'uiCanExit';
+
+    /** Name of state to listen exit from */
+    fromState: string;
+
+    constructor(private $element: JQLite, private $transitions: TransitionService) {}
+
+    $onInit() {
+        const data = this.$element.data();
+        const {CALLBACK_NAME} = this.constructor;
+
+        const controllerWithCallback = Object.keys(data)
+            .map((key) => data[key])
+            .find((controller) => controller[CALLBACK_NAME]);
+
+        if (!controllerWithCallback)
+            return;
+
+        this.off = this.$transitions.onBefore({from: this.fromState}, (...args) => {
+            return controllerWithCallback[CALLBACK_NAME](...args);
+        });
+    }
+
+    $onDestroy() {
+        if (this.off)
+            this.off();
+
+        this.$element = null;
+    }
+}
+
+export default function fakeUiCanExit() {
+    return {
+        bindToController: {
+            fromState: '@fakeUiCanExit'
+        },
+        controller: FakeUiCanExitController
+    };
+}
diff --git a/modules/frontend/app/configuration/components/formUICanExitGuard.ts b/modules/frontend/app/configuration/components/formUICanExitGuard.ts
new file mode 100644
index 0000000..0e1b14e
--- /dev/null
+++ b/modules/frontend/app/configuration/components/formUICanExitGuard.ts
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {default as ConfigChangesGuard} from '../services/ConfigChangesGuard';
+
+class FormUICanExitGuardController {
+    static $inject = ['$element', 'ConfigChangesGuard'];
+
+    constructor(private $element: JQLite, private ConfigChangesGuard: ConfigChangesGuard) {}
+
+    $onDestroy() {
+        this.$element = null;
+    }
+
+    $onInit() {
+        const data = this.$element.data();
+        const controller = Object.keys(data)
+            .map((key) => data[key])
+            .find(this._itQuacks);
+
+        if (!controller)
+            return;
+
+        controller.uiCanExit = ($transition$) => {
+            const options = $transition$.options();
+
+            if (options.custom.justIDUpdate || options.redirectedFrom)
+                return true;
+
+            $transition$.onSuccess({}, controller.reset);
+
+            return this.ConfigChangesGuard.guard(...controller.getValuesToCompare());
+        };
+    }
+
+    _itQuacks(controller) {
+        return controller.reset instanceof Function &&
+            controller.getValuesToCompare instanceof Function &&
+            !controller.uiCanExit;
+    }
+}
+
+export default function formUiCanExitGuard() {
+    return {
+        priority: 10,
+        controller: FormUICanExitGuardController
+    };
+}
diff --git a/modules/frontend/app/configuration/components/modal-import-models/component.js b/modules/frontend/app/configuration/components/modal-import-models/component.js
new file mode 100644
index 0000000..dae0eb3
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-import-models/component.js
@@ -0,0 +1,1204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import templateUrl from './template.tpl.pug';
+import './style.scss';
+import _ from 'lodash';
+import naturalCompare from 'natural-compare-lite';
+import find from 'lodash/fp/find';
+import get from 'lodash/fp/get';
+import {combineLatest, EMPTY, from, merge, of, race, timer} from 'rxjs';
+import {distinctUntilChanged, filter, map, pluck, switchMap, take, tap} from 'rxjs/operators';
+import ObjectID from 'bson-objectid';
+import {uniqueName} from 'app/utils/uniqueName';
+import {defaultNames} from '../../defaultNames';
+// eslint-disable-next-line
+import {UIRouter} from '@uirouter/angularjs'
+import {default as IgniteConfirmBatch} from 'app/services/ConfirmBatch.service';
+import {default as ConfigSelectors} from '../../store/selectors';
+import {default as ConfigEffects} from '../../store/effects';
+import {default as ConfigureState} from '../../services/ConfigureState';
+// eslint-disable-next-line
+import {default as AgentManager} from 'app/modules/agent/AgentModal.service'
+import {default as SqlTypes} from 'app/services/SqlTypes.service';
+import {default as JavaTypes} from 'app/services/JavaTypes.service';
+// eslint-disable-next-line
+import {default as ActivitiesData} from 'app/core/activities/Activities.data';
+
+function _mapCaches(caches = []) {
+    return caches.map((cache) => {
+        return {label: cache.name, value: cache._id, cache};
+    });
+}
+
+const INFO_CONNECT_TO_DB = 'Configure connection to database';
+const INFO_SELECT_SCHEMAS = 'Select schemas to load tables from';
+const INFO_SELECT_TABLES = 'Select tables to import as domain model';
+const INFO_SELECT_OPTIONS = 'Select import domain model options';
+const LOADING_JDBC_DRIVERS = {text: 'Loading JDBC drivers...'};
+const LOADING_SCHEMAS = {text: 'Loading schemas...'};
+const LOADING_TABLES = {text: 'Loading tables...'};
+const SAVING_DOMAINS = {text: 'Saving domain model...'};
+
+const IMPORT_DM_NEW_CACHE = 1;
+const IMPORT_DM_ASSOCIATE_CACHE = 2;
+
+const DFLT_PARTITIONED_CACHE = {
+    label: 'PARTITIONED',
+    value: -1,
+    cache: {
+        name: 'PARTITIONED',
+        cacheMode: 'PARTITIONED',
+        atomicityMode: 'ATOMIC',
+        readThrough: true,
+        writeThrough: true
+    }
+};
+
+const DFLT_REPLICATED_CACHE = {
+    label: 'REPLICATED',
+    value: -2,
+    cache: {
+        name: 'REPLICATED',
+        cacheMode: 'REPLICATED',
+        atomicityMode: 'ATOMIC',
+        readThrough: true,
+        writeThrough: true
+    }
+};
+
+const CACHE_TEMPLATES = [DFLT_PARTITIONED_CACHE, DFLT_REPLICATED_CACHE];
+
+export class ModalImportModels {
+    /**
+     * Cluster ID to import models into
+     * @type {string}
+     */
+    clusterID;
+
+    /** @type {ng.ICompiledExpression} */
+    onHide;
+
+    static $inject = ['$uiRouter', 'ConfigSelectors', 'ConfigEffects', 'ConfigureState', 'IgniteConfirm', 'IgniteConfirmBatch', 'IgniteFocus', 'SqlTypes', 'JavaTypes', 'IgniteMessages', '$scope', '$rootScope', 'AgentManager', 'IgniteActivitiesData', 'IgniteLoading', 'IgniteFormUtils', 'IgniteLegacyUtils'];
+
+    /**
+     * @param {UIRouter} $uiRouter
+     * @param {ConfigSelectors} ConfigSelectors
+     * @param {ConfigEffects} ConfigEffects
+     * @param {ConfigureState} ConfigureState
+     * @param {IgniteConfirmBatch} ConfirmBatch
+     * @param {SqlTypes} SqlTypes
+     * @param {JavaTypes} JavaTypes
+     * @param {ng.IScope} $scope
+     * @param {ng.IRootScopeService} $root
+     * @param {AgentManager} agentMgr
+     * @param {ActivitiesData} ActivitiesData
+     */
+    constructor($uiRouter, ConfigSelectors, ConfigEffects, ConfigureState, Confirm, ConfirmBatch, Focus, SqlTypes, JavaTypes, Messages, $scope, $root, agentMgr, ActivitiesData, Loading, FormUtils, LegacyUtils) {
+        this.$uiRouter = $uiRouter;
+        this.ConfirmBatch = ConfirmBatch;
+        this.ConfigSelectors = ConfigSelectors;
+        this.ConfigEffects = ConfigEffects;
+        this.ConfigureState = ConfigureState;
+        this.$root = $root;
+        this.$scope = $scope;
+        this.agentMgr = agentMgr;
+        this.JavaTypes = JavaTypes;
+        this.SqlTypes = SqlTypes;
+        this.ActivitiesData = ActivitiesData;
+        Object.assign(this, {Confirm, Focus, Messages, Loading, FormUtils, LegacyUtils});
+    }
+
+    loadData() {
+        return of(this.clusterID).pipe(
+            switchMap((id = 'new') => {
+                return this.ConfigureState.state$.pipe(this.ConfigSelectors.selectClusterToEdit(id, defaultNames.importedCluster));
+            }),
+            switchMap((cluster) => {
+                return (!(cluster.caches || []).length && !(cluster.models || []).length)
+                    ? of({
+                        cluster,
+                        caches: [],
+                        models: []
+                    })
+                    : from(Promise.all([
+                        this.ConfigEffects.etp('LOAD_SHORT_CACHES', {ids: cluster.caches || [], clusterID: cluster._id}),
+                        this.ConfigEffects.etp('LOAD_SHORT_MODELS', {ids: cluster.models || [], clusterID: cluster._id})
+                    ])).pipe(switchMap(() => {
+                        return combineLatest(
+                            this.ConfigureState.state$.pipe(this.ConfigSelectors.selectShortCachesValue()),
+                            this.ConfigureState.state$.pipe(this.ConfigSelectors.selectShortModelsValue()),
+                            (caches, models) => ({
+                                cluster,
+                                caches,
+                                models
+                            })
+                        ).pipe(take(1));
+                    }));
+            }),
+            take(1)
+        );
+    }
+
+    saveBatch(batch) {
+        if (!batch.length)
+            return;
+
+        this.$scope.importDomain.loadingOptions = SAVING_DOMAINS;
+        this.Loading.start('importDomainFromDb');
+
+        this.ConfigureState.dispatchAction({
+            type: 'ADVANCED_SAVE_COMPLETE_CONFIGURATION',
+            changedItems: this.batchActionsToRequestBody(batch),
+            prevActions: []
+        });
+
+        this.saveSubscription = race(
+            this.ConfigureState.actions$.pipe(
+                filter((a) => a.type === 'ADVANCED_SAVE_COMPLETE_CONFIGURATION_OK'),
+                tap(() => this.onHide())
+            ),
+            this.ConfigureState.actions$.pipe(
+                filter((a) => a.type === 'ADVANCED_SAVE_COMPLETE_CONFIGURATION_ERR')
+            )
+        ).pipe(
+            take(1),
+            tap(() => {
+                this.Loading.finish('importDomainFromDb');
+            })
+        )
+        .subscribe();
+    }
+
+    batchActionsToRequestBody(batch) {
+        const result = batch.reduce((req, action) => {
+            return {
+                ...req,
+                cluster: {
+                    ...req.cluster,
+                    models: [...req.cluster.models, action.newDomainModel._id],
+                    caches: [...req.cluster.caches, ...action.newDomainModel.caches]
+                },
+                models: [...req.models, action.newDomainModel],
+                caches: action.newCache
+                    ? [...req.caches, action.newCache]
+                    : action.cacheStoreChanges
+                        ? [...req.caches, {
+                            ...this.loadedCaches[action.cacheStoreChanges[0].cacheId],
+                            ...action.cacheStoreChanges[0].change
+                        }]
+                        : req.caches
+            };
+        }, {cluster: this.cluster, models: [], caches: [], igfss: []});
+        result.cluster.models = [...new Set(result.cluster.models)];
+        result.cluster.caches = [...new Set(result.cluster.caches)];
+        return result;
+    }
+
+    onTableSelectionChange(selected) {
+        this.$scope.$applyAsync(() => {
+            this.$scope.importDomain.tablesToUse = selected;
+            this.selectedTablesIDs = selected.map((t) => t.id);
+        });
+    }
+
+    onSchemaSelectionChange(selected) {
+        this.$scope.$applyAsync(() => {
+            this.$scope.importDomain.schemasToUse = selected;
+            this.selectedSchemasIDs = selected.map((i) => i.name);
+        });
+    }
+
+    onVisibleRowsChange(rows) {
+        return this.visibleTables = rows.map((r) => r.entity);
+    }
+
+    onCacheSelect(cacheID) {
+        if (cacheID < 0)
+            return;
+
+        if (this.loadedCaches[cacheID])
+            return;
+
+        return this.onCacheSelectSubcription = merge(
+            timer(0, 1).pipe(
+                take(1),
+                tap(() => this.ConfigureState.dispatchAction({type: 'LOAD_CACHE', cacheID}))
+            ),
+            race(
+                this.ConfigureState.actions$.pipe(
+                    filter((a) => a.type === 'LOAD_CACHE_OK' && a.cache._id === cacheID),
+                    pluck('cache'),
+                    tap((cache) => {
+                        this.loadedCaches[cacheID] = cache;
+                    })
+                ),
+                this.ConfigureState.actions$.pipe(
+                    filter((a) => a.type === 'LOAD_CACHE_ERR' && a.action.cacheID === cacheID)
+                )
+            ).pipe(take(1))
+        )
+        .subscribe();
+    }
+
+    $onDestroy() {
+        this.subscribers$.unsubscribe();
+        if (this.onCacheSelectSubcription) this.onCacheSelectSubcription.unsubscribe();
+        if (this.saveSubscription) this.saveSubscription.unsubscribe();
+    }
+
+    $onInit() {
+        // Restores old behavior
+        const {Confirm, ConfirmBatch, Focus, SqlTypes, JavaTypes, Messages, $scope, $root, agentMgr, ActivitiesData, Loading, FormUtils, LegacyUtils} = this;
+
+        /**
+         * Convert some name to valid java package name.
+         *
+         * @param name to convert.
+         * @returns {string} Valid java package name.
+         */
+        const _toJavaPackage = (name) => {
+            return name ? name.replace(/[^A-Za-z_0-9/.]+/g, '_') : 'org';
+        };
+
+        const importDomainModal = {
+            hide: () => {
+                agentMgr.stopWatch();
+                this.onHide();
+            }
+        };
+
+        const _makeDefaultPackageName = (user) => user
+            ? _toJavaPackage(`${user.email.replace('@', '.').split('.').reverse().join('.')}.model`)
+            : void 0;
+
+        this.$scope.ui = {
+            generatePojo: true,
+            builtinKeys: true,
+            generateKeyFields: true,
+            usePrimitives: true,
+            generateTypeAliases: true,
+            generateFieldAliases: true,
+            packageNameUserInput: _makeDefaultPackageName($root.user)
+        };
+        this.$scope.$hide = importDomainModal.hide;
+
+        this.$scope.importCommon = {};
+
+        this.subscription = this.loadData().pipe(tap((data) => {
+            this.$scope.caches = _mapCaches(data.caches);
+            this.$scope.domains = data.models;
+            this.caches = data.caches;
+            this.cluster = data.cluster;
+
+            if (!_.isEmpty(this.$scope.caches)) {
+                this.$scope.importActions.push({
+                    label: 'Associate with existing cache',
+                    shortLabel: 'Associate',
+                    value: IMPORT_DM_ASSOCIATE_CACHE
+                });
+            }
+            this.$scope.$watch('importCommon.action', this._fillCommonCachesOrTemplates(this.$scope.importCommon), true);
+            this.$scope.importCommon.action = IMPORT_DM_NEW_CACHE;
+        }));
+
+        // New
+        this.loadedCaches = {
+            ...CACHE_TEMPLATES.reduce((a, c) => ({...a, [c.value]: c.cache}), {})
+        };
+
+        this.actions = [
+            {value: 'connect', label: this.$root.IgniteDemoMode ? 'Description' : 'Connection'},
+            {value: 'schemas', label: 'Schemas'},
+            {value: 'tables', label: 'Tables'},
+            {value: 'options', label: 'Options'}
+        ];
+
+        // Legacy
+        $scope.ui.invalidKeyFieldsTooltip = 'Found key types without configured key fields<br/>' +
+            'It may be a result of import tables from database without primary keys<br/>' +
+            'Key field for such key types should be configured manually';
+
+        $scope.indexType = LegacyUtils.mkOptions(['SORTED', 'FULLTEXT', 'GEOSPATIAL']);
+
+        $scope.importActions = [{
+            label: 'Create new cache by template',
+            shortLabel: 'Create',
+            value: IMPORT_DM_NEW_CACHE
+        }];
+
+
+        const _dbPresets = [
+            {
+                db: 'Oracle',
+                jdbcDriverClass: 'oracle.jdbc.OracleDriver',
+                jdbcUrl: 'jdbc:oracle:thin:@[host]:[port]:[database]',
+                user: 'system'
+            },
+            {
+                db: 'DB2',
+                jdbcDriverClass: 'com.ibm.db2.jcc.DB2Driver',
+                jdbcUrl: 'jdbc:db2://[host]:[port]/[database]',
+                user: 'db2admin'
+            },
+            {
+                db: 'SQLServer',
+                jdbcDriverClass: 'com.microsoft.sqlserver.jdbc.SQLServerDriver',
+                jdbcUrl: 'jdbc:sqlserver://[host]:[port][;databaseName=database]'
+            },
+            {
+                db: 'PostgreSQL',
+                jdbcDriverClass: 'org.postgresql.Driver',
+                jdbcUrl: 'jdbc:postgresql://[host]:[port]/[database]',
+                user: 'sa'
+            },
+            {
+                db: 'MySQL',
+                jdbcDriverClass: 'com.mysql.jdbc.Driver',
+                jdbcUrl: 'jdbc:mysql://[host]:[port]/[database]',
+                user: 'root'
+            },
+            {
+                db: 'MySQL',
+                jdbcDriverClass: 'com.mysql.cj.jdbc.Driver',
+                jdbcUrl: 'jdbc:mysql://[host]:[port]/[database]',
+                user: 'root'
+            },
+            {
+                db: 'MySQL',
+                jdbcDriverClass: 'org.mariadb.jdbc.Driver',
+                jdbcUrl: 'jdbc:mariadb://[host]:[port]/[database]',
+                user: 'root'
+            },
+            {
+                db: 'H2',
+                jdbcDriverClass: 'org.h2.Driver',
+                jdbcUrl: 'jdbc:h2:tcp://[host]/[database]',
+                user: 'sa'
+            }
+        ];
+
+        $scope.selectedPreset = {
+            db: 'Generic',
+            jdbcDriverJar: '',
+            jdbcDriverClass: '',
+            jdbcUrl: 'jdbc:[database]',
+            user: 'sa',
+            password: '',
+            tablesOnly: true
+        };
+
+        $scope.demoConnection = {
+            db: 'H2',
+            jdbcDriverClass: 'org.h2.Driver',
+            jdbcUrl: 'jdbc:h2:mem:demo-db',
+            user: 'sa',
+            password: '',
+            tablesOnly: true
+        };
+
+        function _loadPresets() {
+            try {
+                const restoredPresets = JSON.parse(localStorage.dbPresets);
+
+                _.forEach(restoredPresets, (restoredPreset) => {
+                    const preset = _.find(_dbPresets, {jdbcDriverClass: restoredPreset.jdbcDriverClass});
+
+                    if (preset) {
+                        preset.jdbcUrl = restoredPreset.jdbcUrl;
+                        preset.user = restoredPreset.user;
+                    }
+                });
+            }
+            catch (ignore) {
+                // No-op.
+            }
+        }
+
+        _loadPresets();
+
+        function _savePreset(preset) {
+            try {
+                const oldPreset = _.find(_dbPresets, {jdbcDriverClass: preset.jdbcDriverClass});
+
+                if (oldPreset)
+                    _.assign(oldPreset, preset);
+                else
+                    _dbPresets.push(preset);
+
+                localStorage.dbPresets = JSON.stringify(_dbPresets);
+            }
+            catch (err) {
+                Messages.showError(err);
+            }
+        }
+
+        function _findPreset(selectedJdbcJar) {
+            let result = _.find(_dbPresets, function(preset) {
+                return preset.jdbcDriverClass === selectedJdbcJar.jdbcDriverClass;
+            });
+
+            if (!result)
+                result = {db: 'Generic', jdbcUrl: 'jdbc:[database]', user: 'admin'};
+
+            result.jdbcDriverJar = selectedJdbcJar.jdbcDriverJar;
+            result.jdbcDriverClass = selectedJdbcJar.jdbcDriverClass;
+            result.jdbcDriverImplementationVersion = selectedJdbcJar.jdbcDriverImplementationVersion;
+
+            return result;
+        }
+
+        function isValidJavaIdentifier(s) {
+            return JavaTypes.validIdentifier(s) && !JavaTypes.isKeyword(s) && JavaTypes.nonBuiltInClass(s) &&
+                SqlTypes.validIdentifier(s) && !SqlTypes.isKeyword(s);
+        }
+
+        function toJavaIdentifier(name) {
+            if (_.isEmpty(name))
+                return 'DB';
+
+            const len = name.length;
+
+            let ident = '';
+
+            let capitalizeNext = true;
+
+            for (let i = 0; i < len; i++) {
+                const ch = name.charAt(i);
+
+                if (ch === ' ' || ch === '_')
+                    capitalizeNext = true;
+                else if (ch === '-') {
+                    ident += '_';
+                    capitalizeNext = true;
+                }
+                else if (capitalizeNext) {
+                    ident += ch.toLocaleUpperCase();
+
+                    capitalizeNext = false;
+                }
+                else
+                    ident += ch.toLocaleLowerCase();
+            }
+
+            return ident;
+        }
+
+        function toJavaClassName(name) {
+            const clazzName = toJavaIdentifier(name);
+
+            if (isValidJavaIdentifier(clazzName))
+                return clazzName;
+
+            return 'Class' + clazzName;
+        }
+
+        function toJavaFieldName(dbName) {
+            const javaName = toJavaIdentifier(dbName);
+
+            const fieldName = javaName.charAt(0).toLocaleLowerCase() + javaName.slice(1);
+
+            if (isValidJavaIdentifier(fieldName))
+                return fieldName;
+
+            return 'field' + javaName;
+        }
+
+        /**
+         * Load list of database schemas.
+         */
+        const _loadSchemas = () => {
+            agentMgr.awaitAgent()
+                .then(function() {
+                    $scope.importDomain.loadingOptions = LOADING_SCHEMAS;
+                    Loading.start('importDomainFromDb');
+
+                    if ($root.IgniteDemoMode)
+                        return agentMgr.schemas($scope.demoConnection);
+
+                    const preset = $scope.selectedPreset;
+
+                    _savePreset(preset);
+
+                    return agentMgr.schemas(preset);
+                })
+                .then((schemaInfo) => {
+                    $scope.importDomain.action = 'schemas';
+                    $scope.importDomain.info = INFO_SELECT_SCHEMAS;
+                    $scope.importDomain.catalog = toJavaIdentifier(schemaInfo.catalog);
+                    $scope.importDomain.schemas = _.map(schemaInfo.schemas, (schema) => ({name: schema}));
+                    $scope.importDomain.schemasToUse = $scope.importDomain.schemas;
+                    this.selectedSchemasIDs = $scope.importDomain.schemas.map((s) => s.name);
+
+                    if ($scope.importDomain.schemas.length === 0)
+                        $scope.importDomainNext();
+                })
+                .catch(Messages.showError)
+                .then(() => Loading.finish('importDomainFromDb'));
+        };
+
+
+        this._importCachesOrTemplates = [];
+
+        $scope.tableActionView = (tbl) => {
+            const cacheName = get('label')(find({value: tbl.cacheOrTemplate}));
+
+            if (tbl.action === IMPORT_DM_NEW_CACHE)
+                return 'Create ' + tbl.generatedCacheName + ' (' + cacheName + ')';
+
+            return 'Associate with ' + cacheName;
+        };
+
+        /**
+         * Load list of database tables.
+         */
+        const _loadTables = () => {
+            agentMgr.awaitAgent()
+                .then(() => {
+                    $scope.importDomain.loadingOptions = LOADING_TABLES;
+                    Loading.start('importDomainFromDb');
+
+                    $scope.importDomain.allTablesSelected = false;
+                    this.selectedTables = [];
+
+                    const preset = $scope.importDomain.demo ? $scope.demoConnection : $scope.selectedPreset;
+
+                    preset.schemas = $scope.importDomain.schemasToUse.map((s) => s.name);
+
+                    return agentMgr.tables(preset);
+                })
+                .then((tables) => {
+                    this._importCachesOrTemplates = CACHE_TEMPLATES.concat($scope.caches);
+
+                    this._fillCommonCachesOrTemplates($scope.importCommon)($scope.importCommon.action);
+
+                    _.forEach(tables, (tbl, idx) => {
+                        tbl.id = idx;
+                        tbl.action = IMPORT_DM_NEW_CACHE;
+                        // tbl.generatedCacheName = toJavaClassName(tbl.table) + 'Cache';
+                        tbl.generatedCacheName = uniqueName(toJavaClassName(tbl.table) + 'Cache', this.caches);
+                        tbl.cacheOrTemplate = DFLT_PARTITIONED_CACHE.value;
+                        tbl.label = tbl.schema + '.' + tbl.table;
+                        tbl.edit = false;
+                    });
+
+                    $scope.importDomain.action = 'tables';
+                    $scope.importDomain.tables = tables;
+                    const tablesToUse = tables.filter((t) => LegacyUtils.isDefined(_.find(t.columns, (col) => col.key)));
+                    this.selectedTablesIDs = tablesToUse.map((t) => t.id);
+                    this.$scope.importDomain.tablesToUse = tablesToUse;
+
+                    $scope.importDomain.info = INFO_SELECT_TABLES;
+                })
+                .catch(Messages.showError)
+                .then(() => Loading.finish('importDomainFromDb'));
+        };
+
+        $scope.applyDefaults = () => {
+            _.forEach(this.visibleTables, (table) => {
+                table.edit = false;
+                table.action = $scope.importCommon.action;
+                table.cacheOrTemplate = $scope.importCommon.cacheOrTemplate;
+            });
+        };
+
+        $scope._curDbTable = null;
+
+        $scope.startEditDbTableCache = (tbl) => {
+            if ($scope._curDbTable) {
+                $scope._curDbTable.edit = false;
+
+                if ($scope._curDbTable.actionWatch) {
+                    $scope._curDbTable.actionWatch();
+                    $scope._curDbTable.actionWatch = null;
+                }
+            }
+
+            $scope._curDbTable = tbl;
+
+            const _fillFn = this._fillCommonCachesOrTemplates($scope._curDbTable);
+
+            _fillFn($scope._curDbTable.action);
+
+            $scope._curDbTable.actionWatch = $scope.$watch('_curDbTable.action', _fillFn, true);
+
+            $scope._curDbTable.edit = true;
+        };
+
+        /**
+         * Show page with import domain models options.
+         */
+        function _selectOptions() {
+            $scope.importDomain.action = 'options';
+            $scope.importDomain.button = 'Save';
+            $scope.importDomain.info = INFO_SELECT_OPTIONS;
+
+            Focus.move('domainPackageName');
+        }
+
+        const _saveDomainModel = (optionsForm) => {
+            if (optionsForm.$invalid)
+                return this.FormUtils.triggerValidation(optionsForm, this.$scope);
+
+            const generatePojo = $scope.ui.generatePojo;
+            const packageName = $scope.ui.packageName;
+
+            const batch = [];
+            const checkedCaches = [];
+
+            let containKey = true;
+            let containDup = false;
+
+            function dbField(name, jdbcType, nullable, unsigned) {
+                const javaTypes = (unsigned && jdbcType.unsigned) ? jdbcType.unsigned : jdbcType.signed;
+                const javaFieldType = (!nullable && javaTypes.primitiveType && $scope.ui.usePrimitives) ? javaTypes.primitiveType : javaTypes.javaType;
+
+                return {
+                    databaseFieldName: name,
+                    databaseFieldType: jdbcType.dbName,
+                    javaType: javaTypes.javaType,
+                    javaFieldName: toJavaFieldName(name),
+                    javaFieldType
+                };
+            }
+
+            _.forEach($scope.importDomain.tablesToUse, (table, curIx, tablesToUse) => {
+                const qryFields = [];
+                const indexes = [];
+                const keyFields = [];
+                const valFields = [];
+                const aliases = [];
+
+                const tableName = table.table;
+                let typeName = toJavaClassName(tableName);
+
+                if (_.find($scope.importDomain.tablesToUse,
+                        (tbl, ix) => ix !== curIx && tableName === tbl.table)) {
+                    typeName = typeName + '_' + toJavaClassName(table.schema);
+
+                    containDup = true;
+                }
+
+                let valType = tableName;
+                let typeAlias;
+
+                if (generatePojo) {
+                    if ($scope.ui.generateTypeAliases && tableName.toLowerCase() !== typeName.toLowerCase())
+                        typeAlias = tableName;
+
+                    valType = _toJavaPackage(packageName) + '.' + typeName;
+                }
+
+                let _containKey = false;
+
+                _.forEach(table.columns, function(col) {
+                    const fld = dbField(col.name, SqlTypes.findJdbcType(col.type), col.nullable, col.unsigned);
+
+                    qryFields.push({name: fld.javaFieldName, className: fld.javaType});
+
+                    const dbName = fld.databaseFieldName;
+
+                    if (generatePojo && $scope.ui.generateFieldAliases &&
+                        SqlTypes.validIdentifier(dbName) && !SqlTypes.isKeyword(dbName) &&
+                        !_.find(aliases, {field: fld.javaFieldName}) &&
+                        fld.javaFieldName.toUpperCase() !== dbName.toUpperCase())
+                        aliases.push({field: fld.javaFieldName, alias: dbName});
+
+                    if (col.key) {
+                        keyFields.push(fld);
+
+                        _containKey = true;
+                    }
+                    else
+                        valFields.push(fld);
+                });
+
+                containKey &= _containKey;
+                if (table.indexes) {
+                    _.forEach(table.indexes, (idx) => {
+                        const idxFields = _.map(idx.fields, (idxFld) => ({
+                            name: toJavaFieldName(idxFld.name),
+                            direction: idxFld.sortOrder
+                        }));
+
+                        indexes.push({
+                            name: idx.name,
+                            indexType: 'SORTED',
+                            fields: idxFields
+                        });
+                    });
+                }
+
+                const domainFound = _.find($scope.domains, (domain) => domain.valueType === valType);
+
+                const batchAction = {
+                    confirm: false,
+                    skip: false,
+                    table,
+                    newDomainModel: {
+                        _id: ObjectID.generate(),
+                        caches: [],
+                        generatePojo
+                    }
+                };
+
+                if (LegacyUtils.isDefined(domainFound)) {
+                    batchAction.newDomainModel._id = domainFound._id;
+                    // Don't touch original caches value
+                    delete batchAction.newDomainModel.caches;
+                    batchAction.confirm = true;
+                }
+
+                Object.assign(batchAction.newDomainModel, {
+                    tableName: typeAlias,
+                    keyType: valType + 'Key',
+                    valueType: valType,
+                    queryMetadata: 'Configuration',
+                    databaseSchema: table.schema,
+                    databaseTable: tableName,
+                    fields: qryFields,
+                    queryKeyFields: _.map(keyFields, (field) => field.javaFieldName),
+                    indexes,
+                    keyFields,
+                    aliases,
+                    valueFields: _.isEmpty(valFields) ? keyFields.slice() : valFields
+                });
+
+                // Use Java built-in type for key.
+                if ($scope.ui.builtinKeys && batchAction.newDomainModel.keyFields.length === 1) {
+                    const newDomain = batchAction.newDomainModel;
+                    const keyField = newDomain.keyFields[0];
+
+                    newDomain.keyType = keyField.javaType;
+                    newDomain.keyFieldName = keyField.javaFieldName;
+
+                    if (!$scope.ui.generateKeyFields) {
+                        // Exclude key column from query fields.
+                        newDomain.fields = _.filter(newDomain.fields, (field) => field.name !== keyField.javaFieldName);
+
+                        newDomain.queryKeyFields = [];
+                    }
+
+                    // Exclude key column from indexes.
+                    _.forEach(newDomain.indexes, (index) => {
+                        index.fields = _.filter(index.fields, (field) => field.name !== keyField.javaFieldName);
+                    });
+
+                    newDomain.indexes = _.filter(newDomain.indexes, (index) => !_.isEmpty(index.fields));
+                }
+
+                // Prepare caches for generation.
+                if (table.action === IMPORT_DM_NEW_CACHE) {
+                    const newCache = _.cloneDeep(this.loadedCaches[table.cacheOrTemplate]);
+
+                    batchAction.newCache = newCache;
+
+                    // const siblingCaches = batch.filter((a) => a.newCache).map((a) => a.newCache);
+                    const siblingCaches = [];
+                    newCache._id = ObjectID.generate();
+                    newCache.name = uniqueName(typeName + 'Cache', this.caches.concat(siblingCaches));
+                    newCache.domains = [batchAction.newDomainModel._id];
+                    batchAction.newDomainModel.caches = [newCache._id];
+
+                    // POJO store factory is not defined in template.
+                    if (!newCache.cacheStoreFactory || newCache.cacheStoreFactory.kind !== 'CacheJdbcPojoStoreFactory') {
+                        const dialect = $scope.importDomain.demo ? 'H2' : $scope.selectedPreset.db;
+
+                        const catalog = $scope.importDomain.catalog;
+
+                        newCache.cacheStoreFactory = {
+                            kind: 'CacheJdbcPojoStoreFactory',
+                            CacheJdbcPojoStoreFactory: {
+                                dataSourceBean: 'ds' + dialect + '_' + catalog,
+                                dialect,
+                                implementationVersion: $scope.selectedPreset.jdbcDriverImplementationVersion
+                            },
+                            CacheJdbcBlobStoreFactory: { connectVia: 'DataSource' }
+                        };
+                    }
+
+                    if (!newCache.readThrough && !newCache.writeThrough) {
+                        newCache.readThrough = true;
+                        newCache.writeThrough = true;
+                    }
+                }
+                else {
+                    const newDomain = batchAction.newDomainModel;
+                    const cacheId = table.cacheOrTemplate;
+
+                    batchAction.newDomainModel.caches = [cacheId];
+
+                    if (!_.includes(checkedCaches, cacheId)) {
+                        const cache = _.find($scope.caches, {value: cacheId}).cache;
+
+                        // TODO: move elsewhere, make sure it still works
+                        const change = LegacyUtils.autoCacheStoreConfiguration(cache, [newDomain]);
+
+                        if (change)
+                            batchAction.cacheStoreChanges = [{cacheId, change}];
+
+                        checkedCaches.push(cacheId);
+                    }
+                }
+
+                batch.push(batchAction);
+            });
+
+            /**
+             * Generate message to show on confirm dialog.
+             *
+             * @param meta Object to confirm.
+             * @returns {string} Generated message.
+             */
+            function overwriteMessage(meta) {
+                return `
+                    Domain model with name &quot;${meta.newDomainModel.databaseTable}&quot; already exists.
+                    Are you sure you want to overwrite it?
+                `;
+            }
+
+            const itemsToConfirm = _.filter(batch, (item) => item.confirm);
+
+            const checkOverwrite = () => {
+                if (itemsToConfirm.length > 0) {
+                    return ConfirmBatch.confirm(overwriteMessage, itemsToConfirm)
+                        .then(() => this.saveBatch(_.filter(batch, (item) => !item.skip)))
+                        .catch(() => Messages.showError('Importing of domain models interrupted by user.'));
+                }
+                return this.saveBatch(batch);
+            };
+
+            const checkDuplicate = () => {
+                if (containDup) {
+                    Confirm.confirm('Some tables have the same name.<br/>' +
+                        'Name of types for that tables will contain schema name too.')
+                        .then(() => checkOverwrite());
+                }
+                else
+                    checkOverwrite();
+            };
+
+            if (containKey)
+                checkDuplicate();
+            else {
+                Confirm.confirm('Some tables have no primary key.<br/>' +
+                    'You will need to configure key type and key fields for such tables after import complete.')
+                    .then(() => checkDuplicate());
+            }
+        };
+
+
+        $scope.importDomainNext = (form) => {
+            if (!$scope.importDomainNextAvailable())
+                return;
+
+            const act = $scope.importDomain.action;
+
+            if (act === 'drivers' && $scope.importDomain.jdbcDriversNotFound)
+                importDomainModal.hide();
+            else if (act === 'connect')
+                _loadSchemas();
+            else if (act === 'schemas')
+                _loadTables();
+            else if (act === 'tables')
+                _selectOptions();
+            else if (act === 'options')
+                _saveDomainModel(form);
+        };
+
+        $scope.nextTooltipText = function() {
+            const importDomainNextAvailable = $scope.importDomainNextAvailable();
+
+            const act = $scope.importDomain.action;
+
+            if (act === 'drivers' && $scope.importDomain.jdbcDriversNotFound)
+                return 'Resolve issue with JDBC drivers<br>Close this dialog and try again';
+
+            if (act === 'connect' && _.isNil($scope.selectedPreset.jdbcDriverClass))
+                return 'Input valid JDBC driver class name';
+
+            if (act === 'connect' && _.isNil($scope.selectedPreset.jdbcUrl))
+                return 'Input valid JDBC URL';
+
+            if (act === 'connect' || act === 'drivers')
+                return 'Click to load list of schemas from database';
+
+            if (act === 'schemas')
+                return importDomainNextAvailable ? 'Click to load list of tables from database' : 'Select schemas to continue';
+
+            if (act === 'tables')
+                return importDomainNextAvailable ? 'Click to show import options' : 'Select tables to continue';
+
+            if (act === 'options')
+                return 'Click to import domain model for selected tables';
+
+            return 'Click to continue';
+        };
+
+        $scope.prevTooltipText = function() {
+            const act = $scope.importDomain.action;
+
+            if (act === 'schemas')
+                return $scope.importDomain.demo ? 'Click to return on demo description step' : 'Click to return on connection configuration step';
+
+            if (act === 'tables')
+                return 'Click to return on schemas selection step';
+
+            if (act === 'options')
+                return 'Click to return on tables selection step';
+        };
+
+        $scope.importDomainNextAvailable = function() {
+            switch ($scope.importDomain.action) {
+                case 'connect':
+                    return !_.isNil($scope.selectedPreset.jdbcDriverClass) && !_.isNil($scope.selectedPreset.jdbcUrl);
+
+                case 'schemas':
+                    return _.isEmpty($scope.importDomain.schemas) || !!get('importDomain.schemasToUse.length')($scope);
+
+                case 'tables':
+                    return !!$scope.importDomain.tablesToUse.length;
+
+                default:
+                    return true;
+            }
+        };
+
+        $scope.importDomainPrev = function() {
+            $scope.importDomain.button = 'Next';
+
+            if ($scope.importDomain.action === 'options') {
+                $scope.importDomain.action = 'tables';
+                $scope.importDomain.info = INFO_SELECT_TABLES;
+            }
+            else if ($scope.importDomain.action === 'tables' && $scope.importDomain.schemas.length > 0) {
+                $scope.importDomain.action = 'schemas';
+                $scope.importDomain.info = INFO_SELECT_SCHEMAS;
+            }
+            else {
+                $scope.importDomain.action = 'connect';
+                $scope.importDomain.info = INFO_CONNECT_TO_DB;
+            }
+        };
+
+        const demo = $root.IgniteDemoMode;
+
+        $scope.importDomain = {
+            demo,
+            action: demo ? 'connect' : 'drivers',
+            jdbcDriversNotFound: demo,
+            schemas: [],
+            allSchemasSelected: false,
+            tables: [],
+            allTablesSelected: false,
+            button: 'Next',
+            info: ''
+        };
+
+        $scope.importDomain.loadingOptions = LOADING_JDBC_DRIVERS;
+
+        const fetchDomainData = () => {
+            return agentMgr.awaitAgent()
+                .then(() => {
+                    ActivitiesData.post({
+                        group: 'configuration',
+                        action: 'configuration/import/model'
+                    });
+
+                    return true;
+                })
+                .then(() => {
+                    if (demo) {
+                        $scope.ui.packageNameUserInput = $scope.ui.packageName;
+                        $scope.ui.packageName = 'model';
+
+                        return;
+                    }
+
+                    // Get available JDBC drivers via agent.
+                    Loading.start('importDomainFromDb');
+
+                    $scope.jdbcDriverJars = [];
+                    $scope.ui.selectedJdbcDriverJar = {};
+
+                    return agentMgr.drivers()
+                        .then((drivers) => {
+                            $scope.ui.packageName = $scope.ui.packageNameUserInput;
+
+                            if (drivers && drivers.length > 0) {
+                                drivers = _.sortBy(drivers, 'jdbcDriverJar');
+
+                                _.forEach(drivers, (drv) => {
+                                    $scope.jdbcDriverJars.push({
+                                        label: drv.jdbcDriverJar,
+                                        value: {
+                                            jdbcDriverJar: drv.jdbcDriverJar,
+                                            jdbcDriverClass: drv.jdbcDriverCls,
+                                            jdbcDriverImplementationVersion: drv.jdbcDriverImplVersion
+                                        }
+                                    });
+                                });
+
+                                $scope.ui.selectedJdbcDriverJar = $scope.jdbcDriverJars[0].value;
+
+                                $scope.importDomain.action = 'connect';
+                                $scope.importDomain.tables = [];
+                                this.selectedTables = [];
+                            }
+                            else {
+                                $scope.importDomain.jdbcDriversNotFound = true;
+                                $scope.importDomain.button = 'Cancel';
+                            }
+                        })
+                        .then(() => {
+                            $scope.importDomain.info = INFO_CONNECT_TO_DB;
+
+                            Loading.finish('importDomainFromDb');
+                        });
+                });
+        };
+
+        this.agentIsAvailable$ = this.agentMgr.connectionSbj.pipe(
+            pluck('state'),
+            distinctUntilChanged(),
+            map((state) => state !== 'AGENT_DISCONNECTED')
+        );
+
+        this.domainData$ = this.agentIsAvailable$.pipe(
+            switchMap((agentIsAvailable) => {
+                if (!agentIsAvailable)
+                    return of(EMPTY);
+
+                return from(fetchDomainData());
+            })
+        );
+
+        this.subscribers$ = merge(
+            this.subscription,
+            this.domainData$
+        ).subscribe();
+
+        $scope.$watch('ui.selectedJdbcDriverJar', function(val) {
+            if (val && !$scope.importDomain.demo) {
+                const foundPreset = _findPreset(val);
+
+                const selectedPreset = $scope.selectedPreset;
+
+                selectedPreset.db = foundPreset.db;
+                selectedPreset.jdbcDriverJar = foundPreset.jdbcDriverJar;
+                selectedPreset.jdbcDriverClass = foundPreset.jdbcDriverClass;
+                selectedPreset.jdbcDriverImplementationVersion = foundPreset.jdbcDriverImplementationVersion;
+                selectedPreset.jdbcUrl = foundPreset.jdbcUrl;
+                selectedPreset.user = foundPreset.user;
+            }
+        }, true);
+    }
+
+    _fillCommonCachesOrTemplates(item) {
+        return (action) => {
+            if (item.cachesOrTemplates)
+                item.cachesOrTemplates.length = 0;
+            else
+                item.cachesOrTemplates = [];
+
+            if (action === IMPORT_DM_NEW_CACHE)
+                item.cachesOrTemplates.push(...CACHE_TEMPLATES);
+
+            if (!_.isEmpty(this.$scope.caches)) {
+                item.cachesOrTemplates.push(...this.$scope.caches);
+                this.onCacheSelect(item.cachesOrTemplates[0].value);
+            }
+
+            if (
+                !_.find(item.cachesOrTemplates, {value: item.cacheOrTemplate}) &&
+                item.cachesOrTemplates.length
+            )
+                item.cacheOrTemplate = item.cachesOrTemplates[0].value;
+        };
+    }
+
+    schemasColumnDefs = [
+        {
+            name: 'name',
+            displayName: 'Name',
+            field: 'name',
+            enableHiding: false,
+            sort: {direction: 'asc', priority: 0},
+            filter: {
+                placeholder: 'Filter by Name…'
+            },
+            visible: true,
+            sortingAlgorithm: naturalCompare,
+            minWidth: 165
+        }
+    ];
+
+    tablesColumnDefs = [
+        {
+            name: 'schema',
+            displayName: 'Schema',
+            field: 'schema',
+            enableHiding: false,
+            enableFiltering: false,
+            sort: {direction: 'asc', priority: 0},
+            visible: true,
+            sortingAlgorithm: naturalCompare,
+            minWidth: 100
+        },
+        {
+            name: 'table',
+            displayName: 'Table',
+            field: 'table',
+            enableHiding: false,
+            enableFiltering: true,
+            filter: {
+                placeholder: 'Filter by Table…'
+            },
+            visible: true,
+            sortingAlgorithm: naturalCompare,
+            minWidth: 200
+        },
+        {
+            name: 'action',
+            displayName: 'Action',
+            field: 'action',
+            enableHiding: false,
+            enableFiltering: false,
+            cellTemplate: `
+                <tables-action-cell
+                    table='row.entity'
+                    on-edit-start='grid.appScope.$ctrl.$scope.startEditDbTableCache($event)'
+                    on-cache-select='grid.appScope.$ctrl.onCacheSelect($event)'
+                    caches='grid.appScope.$ctrl._importCachesOrTemplates'
+                    import-actions='grid.appScope.$ctrl.$scope.importActions'
+                ></tables-action-cell>
+            `,
+            visible: true,
+            minWidth: 450
+        }
+    ];
+}
+
+export const component = {
+    name: 'modalImportModels',
+    controller: ModalImportModels,
+    templateUrl,
+    bindings: {
+        onHide: '&',
+        clusterID: '<clusterId'
+    }
+};
diff --git a/modules/frontend/app/configuration/components/modal-import-models/index.ts b/modules/frontend/app/configuration/components/modal-import-models/index.ts
new file mode 100644
index 0000000..3bc71da
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-import-models/index.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import {component} from './component';
+import service from './service';
+import {component as stepIndicator} from './step-indicator/component';
+import {component as tablesActionCell} from './tables-action-cell/component';
+import {component as amountIndicator} from './selected-items-amount-indicator/component';
+
+export default angular
+    .module('configuration.modal-import-models', [])
+    .service('ModalImportModels', service)
+    .component('tablesActionCell', tablesActionCell)
+    .component('modalImportModelsStepIndicator', stepIndicator)
+    .component('selectedItemsAmountIndicator', amountIndicator)
+    .component('modalImportModels', component);
diff --git a/modules/frontend/app/configuration/components/modal-import-models/selected-items-amount-indicator/component.ts b/modules/frontend/app/configuration/components/modal-import-models/selected-items-amount-indicator/component.ts
new file mode 100644
index 0000000..abee6b0
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-import-models/selected-items-amount-indicator/component.ts
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+
+export const component = {
+    template,
+    bindings: {
+        selectedAmount: '<',
+        totalAmount: '<'
+    }
+};
diff --git a/modules/frontend/app/configuration/components/modal-import-models/selected-items-amount-indicator/style.scss b/modules/frontend/app/configuration/components/modal-import-models/selected-items-amount-indicator/style.scss
new file mode 100644
index 0000000..6960ddb
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-import-models/selected-items-amount-indicator/style.scss
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+selected-items-amount-indicator {
+    font-size: 14px;
+    font-style: italic;
+    color: #757575;
+    display: inline-block;
+}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/modal-import-models/selected-items-amount-indicator/template.pug b/modules/frontend/app/configuration/components/modal-import-models/selected-items-amount-indicator/template.pug
new file mode 100644
index 0000000..9a46a77
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-import-models/selected-items-amount-indicator/template.pug
@@ -0,0 +1,17 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+| {{ $ctrl.selectedAmount || 0}} of {{ $ctrl.totalAmount }} selected
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/modal-import-models/service.ts b/modules/frontend/app/configuration/components/modal-import-models/service.ts
new file mode 100644
index 0000000..c6aaa6f
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-import-models/service.ts
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {StateDeclaration, StateService, UIRouter} from '@uirouter/angularjs';
+import AgentManager from 'app/modules/agent/AgentManager.service';
+
+export default class ModalImportModels {
+    static $inject = ['$modal', '$q', '$uiRouter', 'AgentManager'];
+
+    deferred: ng.IDeferred<true>;
+    private _state: StateDeclaration;
+    private _modal: mgcrea.ngStrap.modal.IModal;
+
+    constructor(
+        private $modal: mgcrea.ngStrap.modal.IModalService,
+        private $q: ng.IQService,
+        private $uiRouter: UIRouter,
+        private AgentManager: AgentManager
+    ) {}
+
+    _goToDynamicState() {
+        if (this.deferred)
+            return this.deferred.promise;
+
+        this.deferred = this.$q.defer();
+
+        if (this._state)
+            this.$uiRouter.stateRegistry.deregister(this._state);
+
+        this._state = this.$uiRouter.stateRegistry.register({
+            name: 'importModels',
+            parent: this.$uiRouter.stateService.current,
+            onEnter: () => {
+                this._open();
+            },
+            onExit: () => {
+                this.AgentManager.stopWatch();
+                this._modal && this._modal.hide();
+            }
+        });
+
+        return this.$uiRouter.stateService.go(this._state, this.$uiRouter.stateService.params)
+            .catch(() => {
+                this.deferred.reject(false);
+                this.deferred = null;
+            });
+    }
+
+    _open() {
+        const self = this;
+
+        this._modal = this.$modal({
+            template: `
+                <modal-import-models
+                    on-hide='$ctrl.onHide()'
+                    cluster-id='$ctrl.$state.params.clusterID'
+                ></modal-import-models>
+            `,
+            controller: ['$state', function($state: StateService) {
+                this.$state = $state;
+
+                this.onHide = () => {
+                    self.deferred.resolve(true);
+
+                    this.$state.go('^');
+                };
+            }],
+            controllerAs: '$ctrl',
+            backdrop: 'static',
+            show: false
+        });
+
+        return this._modal.$promise
+            .then(() => this._modal.show())
+            .then(() => this.deferred.promise)
+            .finally(() => this.deferred = null);
+    }
+
+    open() {
+        this._goToDynamicState();
+    }
+}
diff --git a/modules/frontend/app/configuration/components/modal-import-models/step-indicator/component.ts b/modules/frontend/app/configuration/components/modal-import-models/step-indicator/component.ts
new file mode 100644
index 0000000..8bc6898
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-import-models/step-indicator/component.ts
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+
+export class ModalImportModelsStepIndicator<T> {
+    steps: Array<{value: T, label: string}>;
+
+    currentStep: T;
+
+    isVisited(index: number) {
+        return index <= this.steps.findIndex((step) => step.value === this.currentStep);
+    }
+}
+
+export const component = {
+    template,
+    controller: ModalImportModelsStepIndicator,
+    bindings: {
+        steps: '<',
+        currentStep: '<'
+    }
+};
diff --git a/modules/frontend/app/configuration/components/modal-import-models/step-indicator/style.scss b/modules/frontend/app/configuration/components/modal-import-models/step-indicator/style.scss
new file mode 100644
index 0000000..677b0f6
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-import-models/step-indicator/style.scss
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+modal-import-models-step-indicator {
+    @import "public/stylesheets/variables.scss";
+
+    $text-color-default: #393939;
+    $text-color-active: $ignite-brand-success;
+    $indicator-color-default: #757575;
+    $indicator-color-active: $ignite-brand-success;
+    $indicator-size: 12px;
+    $indicator-border-radius: 2px;
+    $spline-height: 1px;
+
+    display: block;
+
+    .step-indicator__steps {
+        display: flex;
+        flex-direction: row;
+        justify-content: space-between;
+        margin: 0;
+        padding: 0;
+        list-style: none;
+    }
+
+    .step-indicator__step {
+        color: $text-color-default;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        position: relative;
+
+        &:before {
+            content: '';
+            display: block;
+            background: $indicator-color-default;
+            width: 100%;
+            height: $spline-height;
+            bottom: $indicator-size / 2;
+            position: absolute;
+        }
+
+        &:after {
+            content: '';
+            display: block;
+            background: $indicator-color-default;
+            width: $indicator-size;
+            height: $indicator-size;
+            border-radius: $indicator-border-radius;
+            margin-top: 5px;
+            z-index: 1;
+        }
+    }
+    .step-indicator__step-first,
+    .step-indicator__step-last {
+        &:before {
+            width: calc(50% - #{$indicator-size} / 2);
+        }
+    }
+    .step-indicator__step-first:before {
+        right: 0;
+    }
+    .step-indicator__step-last:before {
+        left: 0;
+    }
+    .step-indicator__step-active {
+        color: $text-color-active;
+
+        &:after {
+            background: $indicator-color-active;            
+        }
+    }
+    .step-indicator__spline {
+        background: $indicator-color-default;
+        height: $spline-height;
+        width: 100%;
+        margin-top: auto;
+        margin-bottom: $indicator-size / 2;
+    }
+    .step-indicator__step-visited {
+        &:before,
+        &+.step-indicator__spline {
+            background: $indicator-color-active;
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/modal-import-models/step-indicator/template.pug b/modules/frontend/app/configuration/components/modal-import-models/step-indicator/template.pug
new file mode 100644
index 0000000..c196604
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-import-models/step-indicator/template.pug
@@ -0,0 +1,31 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+nav
+    .step-indicator__steps
+        .step-indicator__step(
+            ng-repeat-start='step in ::$ctrl.steps'
+            ng-class=`{
+                "step-indicator__step-active": $ctrl.currentStep === step.value,
+                "step-indicator__step-visited": $ctrl.isVisited($index),
+                "step-indicator__step-first": $first,
+                "step-indicator__step-last": $last
+            }`
+        ) {{::step.label}}
+        .step-indicator__spline(
+            ng-repeat-end
+            ng-if='!$last'
+        )
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/modal-import-models/style.scss b/modules/frontend/app/configuration/components/modal-import-models/style.scss
new file mode 100644
index 0000000..b0a5877
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-import-models/style.scss
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+modal-import-models {
+    .modal-content {
+        min-height: 493px;
+        display: flex;
+        flex-direction: column;
+
+        .modal-body {
+            flex: 1 1 auto;
+            overflow-y: auto;
+        }
+        .modal-footer {
+            display: flex;
+            align-items: baseline;
+
+            selected-items-amount-indicator {
+                margin-left: auto;
+                margin-right: auto;
+            }
+        }
+    }
+
+    pc-items-table {
+        width: 100%;
+    }
+
+    modal-import-models-step-indicator {
+        margin-top: 15px;
+        margin-bottom: 30px;
+    }
+    .#{&}__prev-button {
+        margin-right: auto !important;
+    }
+    .#{&}__next-button {
+        margin-left: auto !important;
+    }
+
+    no-data {
+        margin: 30px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 300px;
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/modal-import-models/tables-action-cell/component.ts b/modules/frontend/app/configuration/components/modal-import-models/tables-action-cell/component.ts
new file mode 100644
index 0000000..4fe2771
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-import-models/tables-action-cell/component.ts
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+import {Menu} from 'app/types';
+
+const IMPORT_DM_NEW_CACHE = 1;
+
+export class TablesActionCell {
+    static $inject = ['$element'];
+
+    constructor(private $element: JQLite) {}
+
+    onEditStart?: ng.ICompiledExpression;
+    onCacheSelect?: ng.ICompiledExpression;
+    table: any;
+    caches: Menu<string>;
+    importActions: any;
+
+    onClick(e: JQueryEventObject) {
+        e.stopPropagation();
+    }
+
+    $postLink() {
+        this.$element.on('click', this.onClick);
+    }
+
+    $onDestroy() {
+        this.$element.off('click', this.onClick);
+        this.$element = null;
+    }
+
+    tableActionView(table) {
+        if (!this.caches)
+            return;
+
+        const cache = this.caches.find((c) => c.value === table.cacheOrTemplate);
+
+        if (!cache)
+            return;
+
+        const cacheName = cache.label;
+
+        if (table.action === IMPORT_DM_NEW_CACHE)
+            return 'Create ' + table.generatedCacheName + ' (' + cacheName + ')';
+
+        return 'Associate with ' + cacheName;
+    }
+}
+
+export const component = {
+    controller: TablesActionCell,
+    bindings: {
+        onEditStart: '&',
+        onCacheSelect: '&?',
+        table: '<',
+        caches: '<',
+        importActions: '<'
+    },
+    template
+};
diff --git a/modules/frontend/app/configuration/components/modal-import-models/tables-action-cell/style.scss b/modules/frontend/app/configuration/components/modal-import-models/tables-action-cell/style.scss
new file mode 100644
index 0000000..e6e6333
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-import-models/tables-action-cell/style.scss
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+tables-action-cell {
+    display: flex;
+    align-items: center;
+    height: 100%;
+    padding-left: 10px;
+    padding-right: 10px;
+
+    .table-action-cell__edit-button {
+        background: none;
+        border: 1px solid transparent;
+
+        &:hover {
+            background: white;
+            border: solid 1px #c5c5c5;
+        }
+    }
+    .table-action-cell__edit-form {
+        display: flex;
+        align-items: center;
+        white-space: nowrap;
+        width: 100%;
+    }
+    .table-action-cell__action-select {
+        flex: 3;
+        margin-right: 5px;
+    }
+    .table-action-cell__cache-select {
+        flex: 6;
+        margin-right: 0;
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/modal-import-models/tables-action-cell/template.pug b/modules/frontend/app/configuration/components/modal-import-models/tables-action-cell/template.pug
new file mode 100644
index 0000000..c64d973
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-import-models/tables-action-cell/template.pug
@@ -0,0 +1,43 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+button.table-action-cell__edit-button.btn-ignite(
+    type='button'
+    b_s-tooltip=''
+    d_ata-title='Click to edit'
+    title='Click to edit'
+    data-placement='top'
+    ng-click='$ctrl.onEditStart({$event: $ctrl.table})'
+    ng-if='!$ctrl.table.edit'
+)
+    | {{ $ctrl.tableActionView($ctrl.table) }}
+.table-action-cell__edit-form(ng-if='$ctrl.table.edit')
+    .table-action-cell__action-select
+        +form-field__dropdown({
+            model: '$ctrl.table.action',
+            options: '$ctrl.importActions',
+            optionLabel: 'shortLabel'
+        })
+
+    .table-action-cell__cache-select
+        +form-field__dropdown({
+            model: '$ctrl.table.cacheOrTemplate',
+            options: '$ctrl.table.cachesOrTemplates'
+        })(
+            ng-change='$ctrl.onCacheSelect({$event: $ctrl.table.cacheOrTemplate})'
+        )
diff --git a/modules/frontend/app/configuration/components/modal-import-models/template.tpl.pug b/modules/frontend/app/configuration/components/modal-import-models/template.tpl.pug
new file mode 100644
index 0000000..9b4c597
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-import-models/template.tpl.pug
@@ -0,0 +1,263 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+mixin td-ellipses-lbl(w, lbl)
+    td.td-ellipsis(width=`${w}` style=`min-width: ${w}; max-width: ${w}`)
+        label #{lbl}
+
+.modal--ignite.modal--wide.modal.modal-domain-import.center(role='dialog')
+    -var tipOpts = {};
+    - tipOpts.container = '.modal-content'
+    - tipOpts.placement = 'right'
+    .modal-dialog
+        .modal-content(ignite-loading='importDomainFromDb' ignite-loading-text='{{importDomain.loadingOptions.text}}')
+            #errors-container.modal-header.header
+                button.close(type='button' ng-click='$hide()' aria-hidden='true')
+                    svg(ignite-icon="cross")
+                h4.modal-title()
+                    span(ng-if='!importDomain.demo') Import domain models from database
+                    span(ng-if='importDomain.demo') Import domain models from demo database
+            .modal-body(ng-if-start='$ctrl.agentIsAvailable$ | async: this')
+                modal-import-models-step-indicator(
+                    steps='$ctrl.actions'
+                    current-step='importDomain.action'
+                )
+                .import-domain-model-wizard-page(ng-if='importDomain.action == "drivers" && !importDomain.jdbcDriversNotFound')
+                .import-domain-model-wizard-page(ng-if='importDomain.action == "drivers" && importDomain.jdbcDriversNotFound')
+                    | Domain model could not be imported
+                    ul
+                        li Agent failed to find JDBC drivers
+                        li Copy required JDBC drivers into agent 'jdbc-drivers' folder and try again
+                        li Refer to agent README.txt for more information
+                .import-domain-model-wizard-page(ng-if='importDomain.action == "connect" && importDomain.demo')
+                    div(ng-if='demoConnection.db == "H2"')
+                        ul
+                            li In-memory H2 database server will be started inside agent.
+                            li Database will be populated with sample tables.
+                            li You could test domain model generation with this demo database.
+                            li Click "Next" to continue.
+                    div(ng-if='demoConnection.db != "H2"')
+                        label Demo could not be started
+                            ul
+                                li Agent failed to resolve H2 database jar
+                                li Copy h2-x.x.x.jar into agent 'jdbc-drivers' folder and try again
+                                li Refer to agent README.txt for more information
+                .import-domain-model-wizard-page(ng-if='importDomain.action == "connect" && !importDomain.demo')
+                    form.pc-form-grid-row(name=form novalidate)
+                        .pc-form-grid-col-30
+                            +form-field__dropdown({
+                                label: 'Driver JAR:',
+                                model: 'ui.selectedJdbcDriverJar',
+                                name: '"jdbcDriverJar"',
+                                required: true,
+                                placeholder: 'Choose JDBC driver',
+                                options: 'jdbcDriverJars',
+                                tip: 'Select appropriate JAR with JDBC driver<br> To add another driver you need to place it into "/jdbc-drivers" folder of Ignite Web Agent<br> Refer to Ignite Web Agent README.txt for for more information'
+                            })
+                        .pc-form-grid-col-30
+                            +form-field__java-class({
+                                label: 'JDBC driver:',
+                                model: 'selectedPreset.jdbcDriverClass',
+                                name: '"jdbcDriverClass"',
+                                required: true,
+                                tip: 'Fully qualified class name of JDBC driver that will be used to connect to database'
+                            })
+                        .pc-form-grid-col-60
+                            +form-field__text({
+                                label: 'JDBC URL:',
+                                model: 'selectedPreset.jdbcUrl',
+                                name: '"jdbcUrl"',
+                                required: true,
+                                placeholder: 'JDBC URL',
+                                tip: 'JDBC URL for connecting to database<br>Refer to your database documentation for details'
+                            })(
+                                ignite-form-field-input-autofocus='true'
+                            )
+
+                        .pc-form-grid-col-30
+                            +form-field__text({
+                                label: 'User:',
+                                model: 'selectedPreset.user',
+                                name: '"jdbcUser"',
+                                tip: 'User name for connecting to database'
+                            })
+                        .pc-form-grid-col-30
+                            +form-field__password({
+                                label: 'Password:',
+                                model: 'selectedPreset.password',
+                                name: '"jdbcPassword"',
+                                tip: 'Password for connecting to database<br>Note, password would not be saved in preferences for security reasons'
+                            })(
+                                ignite-on-enter='importDomainNext()'
+                            )
+                        .pc-form-grid-col-60
+                            +form-field__checkbox({
+                                label: 'Tables only',
+                                model: 'selectedPreset.tablesOnly',
+                                name: '"tablesOnly"',
+                                tip: 'If selected, then only tables metadata will be parsed<br>Otherwise table and view metadata will be parsed',
+                                tipOpts
+                            })
+
+                .import-domain-model-wizard-page(ng-if='importDomain.action == "schemas"')
+                    pc-items-table(
+                        column-defs='::$ctrl.schemasColumnDefs'
+                        items='importDomain.schemas'
+                        hide-header='::true'
+                        one-way-selection='::true'
+                        selected-row-id='$ctrl.selectedSchemasIDs'
+                        on-selection-change='$ctrl.onSchemaSelectionChange($event)'
+                        row-identity-key='name'
+                    )
+                .import-domain-model-wizard-page(ng-if='importDomain.action == "tables"')
+                    form.pc-form-grid-row(novalidate)
+                        .pc-form-grid-col-30
+                            +form-field__dropdown({
+                                label: 'Action:',
+                                model: 'importCommon.action'
+                            })(
+                                bs-options='item.value as item.label for item in importActions'
+                            )
+                        .pc-form-grid-col-30
+                            +form-field__dropdown({
+                                label: 'Cache:',
+                                model: 'importCommon.cacheOrTemplate'
+                            })(
+                                bs-options='item.value as item.label for item in importCommon.cachesOrTemplates'
+                                ng-change='$ctrl.onCacheSelect(importCommon.cacheOrTemplate)'
+                            )
+                        .pc-form-grid-col-60.pc-form-grid__text-only-item
+                            +form-field__label({ label: 'Defaults to be applied for filtered tables' })
+                                +form-field__tooltip({ title: 'Select and apply options for caches generation' })
+                            button.btn-ignite.btn-ignite--success(
+                                type='button'
+                                ng-click='applyDefaults()'
+                                style='margin-left: auto'
+                            ) Apply
+                    pc-items-table(
+                        column-defs='::$ctrl.tablesColumnDefs'
+                        items='importDomain.tables'
+                        hide-header='::true'
+                        on-visible-rows-change='$ctrl.onVisibleRowsChange($event)'
+                        one-way-selection='::true'
+                        selected-row-id='$ctrl.selectedTablesIDs'
+                        on-selection-change='$ctrl.onTableSelectionChange($event)'
+                        row-identity-key='id'
+                    )
+                .import-domain-model-wizard-page(ng-show='importDomain.action == "options"')
+                    -var form = 'optionsForm'
+                    -var generatePojo = 'ui.generatePojo'
+
+                    form.pc-form-grid-row(name=form novalidate)
+                        .pc-form-grid-col-60
+                            +form-field__checkbox({
+                                label: 'Use Java built-in types for keys',
+                                model: 'ui.builtinKeys',
+                                name: '"domainBuiltinKeys"',
+                                tip: 'Use Java built-in types like "Integer", "Long", "String" instead of POJO generation in case when table primary key contains only one field',
+                                tipOpts
+                            })
+                        .pc-form-grid-col-60
+                            +form-field__checkbox({
+                                label: 'Use primitive types for NOT NULL table columns',
+                                model: 'ui.usePrimitives',
+                                name: '"domainUsePrimitives"',
+                                tip: 'Use primitive types like "int", "long", "double" for POJOs fields generation in case of NOT NULL columns',
+                                tipOpts
+                            })
+                        .pc-form-grid-col-60
+                            +form-field__checkbox({
+                                label: 'Generate query entity key fields',
+                                model: 'ui.generateKeyFields',
+                                name: '"generateKeyFields"',
+                                tip: 'Generate key fields for query entity.<br\>\
+                                We need this for the cases when no key-value classes\
+                                are present on cluster nodes, and we need to build/modify keys and values during SQL DML operations.\
+                                Thus, setting this parameter is not mandatory and should be based on particular use case.',
+                                tipOpts
+                            })
+                        .pc-form-grid-col-60
+                            +form-field__checkbox({
+                                label: 'Generate POJO classes',
+                                model: generatePojo,
+                                name: '"domainGeneratePojo"',
+                                tip: 'If selected then POJO classes will be generated from database tables',
+                                tipOpts
+                            })
+                        .pc-form-grid-col-60(ng-if=generatePojo)
+                            +form-field__checkbox({
+                                label: 'Generate aliases for query entity',
+                                model: 'ui.generateTypeAliases',
+                                name: '"domainGenerateTypeAliases"',
+                                tip: 'Generate aliases for query entity if table name is invalid Java identifier',
+                                tipOpts
+                            })
+                        .pc-form-grid-col-60(ng-if=generatePojo)
+                            +form-field__checkbox({
+                                label: 'Generate aliases for query fields',
+                                model: 'ui.generateFieldAliases',
+                                name: '"domainGenerateFieldAliases"',
+                                tip: 'Generate aliases for query fields with database field names when database field name differ from Java field name',
+                                tipOpts
+                            })
+                        .pc-form-grid-col-60(ng-if=generatePojo)
+                            +form-field__java-package({
+                                label: 'Package:',
+                                model: 'ui.packageName',
+                                name: '"domainPackageName"',
+                                required: true,
+                                tip: 'Package that will be used for POJOs generation',
+                                tipOpts
+                            })
+
+            .modal-footer(ng-if-end)
+                button.btn-ignite.btn-ignite--success.modal-import-models__prev-button(
+                    type='button'
+                    ng-hide='importDomain.action == "drivers" || importDomain.action == "connect"'
+                    ng-click='importDomainPrev()'
+                    b_s-tooltip=''
+                    d_ata-title='{{prevTooltipText()}}'
+                    d_ata-placement='bottom'
+                ) Prev
+                selected-items-amount-indicator(
+                    ng-if='$ctrl.$scope.importDomain.action === "schemas"'
+                    selected-amount='$ctrl.selectedSchemasIDs.length'
+                    total-amount='$ctrl.$scope.importDomain.schemas.length'
+                )
+                selected-items-amount-indicator(
+                    ng-if='$ctrl.$scope.importDomain.action === "tables"'
+                    selected-amount='$ctrl.selectedTablesIDs.length'
+                    total-amount='$ctrl.$scope.importDomain.tables.length'
+                )
+                button.btn-ignite.btn-ignite--success.modal-import-models__next-button(
+                    type='button'
+                    ng-click='importDomainNext(optionsForm)'
+                    ng-disabled='!importDomainNextAvailable()'
+                    b_s-tooltip=''
+                    d_ata-title='{{nextTooltipText()}}'
+                    d_ata-placement='bottom'
+                )
+                    svg.icon-left(ignite-icon='checkmark' ng-show='importDomain.button === "Save"')
+                    | {{importDomain.button}}
+
+            .modal-body(ng-if-start='!($ctrl.agentIsAvailable$ | async: this)')
+                no-data
+
+            .modal-footer(ng-if-end)
+                button.btn-ignite.btn-ignite--success.modal-import-models__next-button(ng-click='$ctrl.onHide()') Cancel
diff --git a/modules/frontend/app/configuration/components/modal-preview-project/component.ts b/modules/frontend/app/configuration/components/modal-preview-project/component.ts
new file mode 100644
index 0000000..462f01d
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-preview-project/component.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+import controller from './controller';
+
+export default {
+    name: 'modalPreviewProject',
+    template,
+    controller,
+    bindings: {
+        onHide: '&',
+        cluster: '<',
+        isDemo: '<'
+    }
+};
diff --git a/modules/frontend/app/configuration/components/modal-preview-project/controller.ts b/modules/frontend/app/configuration/components/modal-preview-project/controller.ts
new file mode 100644
index 0000000..8923128
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-preview-project/controller.ts
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import JSZip from 'jszip';
+
+import PageConfigure from '../../services/PageConfigure';
+import ConfigurationResourceFactory from '../../services/ConfigurationResource';
+import SummaryZipperFactory from '../../services/SummaryZipper';
+import IgniteVersion from 'app/services/Version.service';
+import ConfigurationDownload from '../../services/ConfigurationDownload';
+import IgniteLoadingFactory from 'app/modules/loading/loading.service';
+import MessagesFactory from 'app/services/Messages.service';
+import {Cluster, ShortCluster} from '../../types';
+
+type CluserLike = Cluster | ShortCluster;
+
+export default class ModalPreviewProjectController {
+    static $inject = [
+        'PageConfigure',
+        'IgniteConfigurationResource',
+        'IgniteSummaryZipper',
+        'IgniteVersion',
+        '$scope',
+        'ConfigurationDownload',
+        'IgniteLoading',
+        'IgniteMessages'
+    ];
+
+    constructor(
+        private PageConfigure: PageConfigure,
+        private IgniteConfigurationResource: ReturnType<typeof ConfigurationResourceFactory>,
+        private summaryZipper: ReturnType<typeof SummaryZipperFactory>,
+        private IgniteVersion: IgniteVersion,
+        private $scope: ng.IScope,
+        private ConfigurationDownload: ConfigurationDownload,
+        private IgniteLoading: ReturnType<typeof IgniteLoadingFactory>,
+        private IgniteMessages: ReturnType<typeof MessagesFactory>
+    ) {}
+
+    onHide: ng.ICompiledExpression;
+    cluster: CluserLike;
+    isDemo: boolean;
+    fileText: string;
+
+    $onInit() {
+        this.treeOptions = {
+            nodeChildren: 'children',
+            dirSelectable: false,
+            injectClasses: {
+                iExpanded: 'fa fa-folder-open-o',
+                iCollapsed: 'fa fa-folder-o'
+            }
+        };
+        this.doStuff(this.cluster, this.isDemo);
+    }
+
+    showPreview(node) {
+        this.fileText = '';
+
+        if (!node)
+            return;
+
+        this.fileExt = node.file.name.split('.').reverse()[0].toLowerCase();
+
+        if (node.file.dir)
+            return;
+
+        node.file.async('string').then((text) => {
+            this.fileText = text;
+            this.$scope.$applyAsync();
+        });
+    }
+
+    doStuff(cluster: CluserLike, isDemo: boolean) {
+        this.IgniteLoading.start('projectStructurePreview');
+        return this.PageConfigure.getClusterConfiguration({clusterID: cluster._id, isDemo})
+        .then((data) => {
+            return this.IgniteConfigurationResource.populate(data);
+        })
+        .then(({clusters}) => {
+            return clusters.find(({_id}) => _id === cluster._id);
+        })
+        .then((cluster) => {
+            return this.summaryZipper({
+                cluster,
+                data: {},
+                IgniteDemoMode: isDemo,
+                targetVer: this.IgniteVersion.currentSbj.getValue()
+            });
+        })
+        .then(JSZip.loadAsync)
+        .then((val) => {
+            const convert = (files) => {
+                return Object.keys(files)
+                .map((path, i, paths) => ({
+                    fullPath: path,
+                    path: path.replace(/\/$/, ''),
+                    file: files[path],
+                    parent: files[paths.filter((p) => path.startsWith(p) && p !== path).sort((a, b) => b.length - a.length)[0]]
+                }))
+                .map((node, i, nodes) => Object.assign(node, {
+                    path: node.parent ? node.path.replace(node.parent.name, '') : node.path,
+                    children: nodes.filter((n) => n.parent && n.parent.name === node.file.name)
+                }));
+            };
+
+            const nodes = convert(val.files);
+
+            this.data = [{
+                path: this.ConfigurationDownload.nameFile(cluster),
+                file: {dir: true},
+                children: nodes.filter((n) => !n.parent)
+            }];
+
+            this.selectedNode = nodes.find((n) => n.path.includes('server.xml'));
+            this.expandedNodes = [
+                ...this.data,
+                ...nodes.filter((n) => {
+                    return !n.fullPath.startsWith('src/main/java/')
+                        || /src\/main\/java(\/(config|load|startup))?\/$/.test(n.fullPath);
+                })
+            ];
+            this.showPreview(this.selectedNode);
+            this.IgniteLoading.finish('projectStructurePreview');
+        })
+        .catch((e) => {
+            this.IgniteMessages.showError('Failed to generate project preview: ', e);
+            this.onHide({});
+        });
+    }
+
+    orderBy() {
+        return;
+    }
+}
diff --git a/modules/frontend/app/configuration/components/modal-preview-project/index.ts b/modules/frontend/app/configuration/components/modal-preview-project/index.ts
new file mode 100644
index 0000000..a8aebb5
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-preview-project/index.ts
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'brace/mode/properties';
+import 'brace/mode/yaml';
+import angular from 'angular';
+import component from './component';
+import service from './service';
+
+export default angular
+    .module('ignite-console.page-configure.modal-preview-project', [])
+    .service('ModalPreviewProject', service)
+    .component(component.name, component);
diff --git a/modules/frontend/app/configuration/components/modal-preview-project/service.ts b/modules/frontend/app/configuration/components/modal-preview-project/service.ts
new file mode 100644
index 0000000..57d21dc
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-preview-project/service.ts
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {ShortCluster} from '../../types';
+
+export default class ModalPreviewProject {
+    static $inject = ['$modal'];
+
+    modalInstance: mgcrea.ngStrap.modal.IModal;
+
+    constructor(private $modal: mgcrea.ngStrap.modal.IModalService) {}
+
+    open(cluster: ShortCluster) {
+        this.modalInstance = this.$modal({
+            locals: {
+                cluster
+            },
+            controller: ['cluster', '$rootScope', function(cluster, $rootScope) {
+                this.cluster = cluster;
+                this.isDemo = !!$rootScope.IgniteDemoMode;
+            }],
+            controllerAs: '$ctrl',
+            template: `
+                <modal-preview-project
+                    on-hide='$hide()'
+                    cluster='::$ctrl.cluster'
+                    is-demo='::$ctrl.isDemo'
+                ></modal-preview-project>
+            `,
+            show: false
+        });
+        return this.modalInstance.$promise.then((modal) => {
+            this.modalInstance.show();
+        });
+    }
+}
diff --git a/modules/frontend/app/configuration/components/modal-preview-project/style.scss b/modules/frontend/app/configuration/components/modal-preview-project/style.scss
new file mode 100644
index 0000000..07a87a2
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-preview-project/style.scss
@@ -0,0 +1,67 @@
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+modal-preview-project {
+    display: block;
+}
+
+.modal-preview-project-structure {
+    @import "public/stylesheets/variables.scss";
+
+    .modal-dialog {
+        width: 900px;
+    }
+    .modal-content .modal-body {
+        display: flex;
+        flex-direction: row;
+        height: 360px;
+        padding-top: 10px;
+        padding-bottom: 0;
+    }
+    .pane-left {
+        width: 330px;
+        overflow: auto;
+        border-right: 1px solid #dddddd;
+    }
+    .pane-right {
+        width: 560px;
+    }
+    treecontrol {
+        white-space: nowrap;
+        font-family: inherit;
+        font-size: 12px;
+        color: #393939;
+
+        ul {
+            overflow: visible;
+        }
+        li {
+            padding-left: 1em;
+        }
+        .fa {
+            margin-right: 5px;
+        }
+        .tree-selected {
+            color: $ignite-brand-success;
+        }
+    }
+    .file-preview {
+        margin: 0;
+        height: 100%;
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/modal-preview-project/template.pug b/modules/frontend/app/configuration/components/modal-preview-project/template.pug
new file mode 100644
index 0000000..536db28
--- /dev/null
+++ b/modules/frontend/app/configuration/components/modal-preview-project/template.pug
@@ -0,0 +1,48 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+.modal.modal--ignite.theme--ignite.center.modal-preview-project-structure(tabindex='-1' role='dialog')
+    .modal-dialog
+        .modal-content
+            .modal-header
+                h4.modal-title
+                    svg(ignite-icon="structure")
+                    span See Project Structure
+                button.close(type='button' aria-label='Close' ng-click='$ctrl.onHide()')
+                     svg(ignite-icon="cross")
+
+            .modal-body(
+                ignite-loading='projectStructurePreview'
+                ignite-loading-text='Generating project structure preview…'
+            )
+                .pane-left
+                    treecontrol(
+                        tree-model='$ctrl.data'
+                        on-selection='$ctrl.showPreview(node)'
+                        selected-node='$ctrl.selectedNode'
+                        expanded-nodes='$ctrl.expandedNodes'
+                        options='$ctrl.treeOptions'
+                        order-by='["file.dir", "-path"]'
+                    )
+                        i.fa.fa-file-text-o(ng-if='::!node.file.dir')
+                        | {{ ::node.path }}
+                .pane-right
+                    div.file-preview(ignite-ace='{mode: $ctrl.fileExt, readonly: true}' ng-model='$ctrl.fileText')
+            .modal-footer
+                div
+                    button.btn-ignite.btn-ignite--success(ng-click='$ctrl.onHide()') Close
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/component.ts b/modules/frontend/app/configuration/components/page-configure-advanced/component.ts
new file mode 100644
index 0000000..bb2f7f7
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/component.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+import controller from './controller';
+
+export default {
+    template,
+    controller
+};
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/component.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/component.ts
new file mode 100644
index 0000000..44ec11b
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/component.ts
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import controller from './controller';
+import templateUrl from './template.tpl.pug';
+import './style.scss';
+
+export default {
+    controller,
+    templateUrl,
+    bindings: {
+        cache: '<',
+        caches: '<',
+        models: '<',
+        igfss: '<',
+        onSave: '&'
+    }
+};
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/controller.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/controller.ts
new file mode 100644
index 0000000..7facc31
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/controller.ts
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import cloneDeep from 'lodash/cloneDeep';
+import get from 'lodash/get';
+import {tap} from 'rxjs/operators';
+import {Menu} from 'app/types';
+
+import LegacyConfirmFactory from 'app/services/Confirm.service';
+import Version from 'app/services/Version.service';
+import Caches from '../../../../services/Caches';
+import FormUtilsFactory from 'app/services/FormUtils.service';
+
+export default class CacheEditFormController {
+    modelsMenu: Menu<string>;
+
+    igfssMenu: Menu<string>;
+
+    /** IGFS IDs to validate against. */
+    igfsIDs: string[];
+
+    onSave: ng.ICompiledExpression;
+
+    static $inject = ['IgniteConfirm', 'IgniteVersion', '$scope', 'Caches', 'IgniteFormUtils'];
+
+    constructor(
+        private IgniteConfirm: ReturnType<typeof LegacyConfirmFactory>,
+        private IgniteVersion: Version,
+        private $scope: ng.IScope,
+        private Caches: Caches,
+        private IgniteFormUtils: ReturnType<typeof FormUtilsFactory>
+    ) {}
+
+    $onInit() {
+        this.available = this.IgniteVersion.available.bind(this.IgniteVersion);
+
+        const rebuildDropdowns = () => {
+            this.$scope.affinityFunction = [
+                {value: 'Rendezvous', label: 'Rendezvous'},
+                {value: 'Custom', label: 'Custom'},
+                {value: null, label: 'Default'}
+            ];
+
+            if (this.available(['1.0.0', '2.0.0']))
+                this.$scope.affinityFunction.splice(1, 0, {value: 'Fair', label: 'Fair'});
+        };
+
+        rebuildDropdowns();
+
+        const filterModel = () => {
+            if (
+                this.clonedCache &&
+                this.available('2.0.0') &&
+                get(this.clonedCache, 'affinity.kind') === 'Fair'
+            )
+                this.clonedCache.affinity.kind = null;
+
+        };
+
+        this.subscription = this.IgniteVersion.currentSbj.pipe(
+            tap(rebuildDropdowns),
+            tap(filterModel)
+        )
+        .subscribe();
+
+        // TODO: Do we really need this?
+        this.$scope.ui = this.IgniteFormUtils.formUI();
+
+        this.formActions = [
+            {text: 'Save', icon: 'checkmark', click: () => this.save()},
+            {text: 'Save and Download', icon: 'download', click: () => this.save(true)}
+        ];
+    }
+
+    $onDestroy() {
+        this.subscription.unsubscribe();
+    }
+
+    $onChanges(changes) {
+        if (
+            'cache' in changes && get(this.clonedCache, '_id') !== get(this.cache, '_id')
+        ) {
+            this.clonedCache = cloneDeep(changes.cache.currentValue);
+            if (this.$scope.ui && this.$scope.ui.inputForm) {
+                this.$scope.ui.inputForm.$setPristine();
+                this.$scope.ui.inputForm.$setUntouched();
+            }
+        }
+        if ('models' in changes)
+            this.modelsMenu = (changes.models.currentValue || []).map((m) => ({value: m._id, label: m.valueType}));
+        if ('igfss' in changes) {
+            this.igfssMenu = (changes.igfss.currentValue || []).map((i) => ({value: i._id, label: i.name}));
+            this.igfsIDs = (changes.igfss.currentValue || []).map((i) => i._id);
+        }
+    }
+
+    getValuesToCompare() {
+        return [this.cache, this.clonedCache].map(this.Caches.normalize);
+    }
+
+    save(download) {
+        if (this.$scope.ui.inputForm.$invalid)
+            return this.IgniteFormUtils.triggerValidation(this.$scope.ui.inputForm, this.$scope);
+        this.onSave({$event: {cache: cloneDeep(this.clonedCache), download}});
+    }
+
+    reset = (forReal) => forReal ? this.clonedCache = cloneDeep(this.cache) : void 0;
+
+    confirmAndReset() {
+        return this.IgniteConfirm.confirm('Are you sure you want to undo all changes for current cache?')
+        .then(this.reset);
+    }
+
+    clearImplementationVersion(storeFactory) {
+        delete storeFactory.implementationVersion;
+    }
+}
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/index.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/index.ts
new file mode 100644
index 0000000..5e0ac6e
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('configuration.cache-edit-form', [])
+    .component('cacheEditForm', component);
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/style.scss b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/style.scss
new file mode 100644
index 0000000..d656f3d
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/style.scss
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+cache-edit-form {
+    display: block;
+}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/template.tpl.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/template.tpl.pug
new file mode 100644
index 0000000..5d98d77
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/template.tpl.pug
@@ -0,0 +1,44 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+form(
+    name='ui.inputForm'
+    id='cache'
+    novalidate
+)
+    include ./templates/general
+    include ./templates/memory
+    include ./templates/query
+    include ./templates/store
+
+    include ./templates/affinity
+    include ./templates/concurrency
+    include ./templates/key-cfg
+    include ./templates/misc
+    include ./templates/near-cache-client
+    include ./templates/near-cache-server
+    include ./templates/node-filter
+    include ./templates/rebalance
+    include ./templates/statistics
+
+.pc-form-actions-panel
+    .pc-form-actions-panel__right-after
+    button.btn-ignite.btn-ignite--link-success(
+        type='button'
+        ng-click='$ctrl.confirmAndReset()'
+    )
+        | Cancel
+    pc-split-button(actions=`::$ctrl.formActions`)
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/affinity.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/affinity.pug
new file mode 100644
index 0000000..291d36d
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/affinity.pug
@@ -0,0 +1,142 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'affinity'
+-var model = '$ctrl.clonedCache'
+-var affModel = model + '.affinity'
+-var rendezvousAff = affModel + '.kind === "Rendezvous"'
+-var fairAff = affModel + '.kind === "Fair"'
+-var customAff = affModel + '.kind === "Custom"'
+-var rendPartitionsRequired = rendezvousAff + ' && ' + affModel + '.Rendezvous.affinityBackupFilter'
+-var fairPartitionsRequired = fairAff + ' && ' + affModel + '.Fair.affinityBackupFilter'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Affinity Collocation
+    panel-description
+        | Collocate data with data to improve performance and scalability of your application.
+        a.link-success(href="https://apacheignite.readme.io/docs/affinity-collocation" target="_blank") More info
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60(ng-if='$ctrl.available(["1.0.0", "2.0.0"])')
+                +form-field__dropdown({
+                    label: 'Function:',
+                    model: `${affModel}.kind`,
+                    name: '"AffinityKind"',
+                    placeholder: 'Default',
+                    options: 'affinityFunction',
+                    tip: 'Key topology resolver to provide mapping from keys to nodes<br/>\
+                                        <ul>\
+                                            <li>Rendezvous - Based on Highest Random Weight algorithm</li>\
+                                            <li>Fair - Tries to ensure that all nodes get equal number of partitions with minimum amount of reassignments between existing nodes</li>\
+                                            <li>Custom - Custom implementation of key affinity fynction</li>\
+                                            <li>Default - By default rendezvous affinity function  with 1024 partitions is used</li>\
+                                        </ul>'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
+                +form-field__dropdown({
+                    label: 'Function:',
+                    model: `${affModel}.kind`,
+                    name: '"AffinityKind"',
+                    placeholder: 'Default',
+                    options: 'affinityFunction',
+                    tip: 'Key topology resolver to provide mapping from keys to nodes<br/>\
+                                       <ul>\
+                                           <li>Rendezvous - Based on Highest Random Weight algorithm</li>\
+                                           <li>Custom - Custom implementation of key affinity fynction</li>\
+                                           <li>Default - By default rendezvous affinity function  with 1024 partitions is used</li>\
+                                       </ul>'
+                })
+            .pc-form-group
+                .pc-form-grid-row(ng-if=rendezvousAff)
+                    .pc-form-grid-col-60
+                        +form-field__number({
+                            label: 'Partitions',
+                            model: `${affModel}.Rendezvous.partitions`,
+                            name: '"RendPartitions"',
+                            required: rendPartitionsRequired,
+                            placeholder: '1024',
+                            min: '1',
+                            tip: 'Number of partitions'
+                        })
+                    .pc-form-grid-col-60
+                        +form-field__java-class({
+                            label: 'Backup filter',
+                            model: `${affModel}.Rendezvous.affinityBackupFilter`,
+                            name: '"RendAffinityBackupFilter"',
+                            tip: 'Backups will be selected from all nodes that pass this filter'
+                        })
+                    .pc-form-grid-col-60
+                        +form-field__checkbox({
+                            label: 'Exclude neighbors',
+                            model: `${affModel}.Rendezvous.excludeNeighbors`,
+                            name: '"RendExcludeNeighbors"',
+                            tip: 'Exclude same - host - neighbors from being backups of each other and specified number of backups'
+                        })
+                .pc-form-grid-row(ng-if=fairAff)
+                    .pc-form-grid-col-60
+                        +form-field__number({
+                            label: 'Partitions',
+                            model: `${affModel}.Fair.partitions`,
+                            name: '"FairPartitions"',
+                            required: fairPartitionsRequired,
+                            placeholder: '256',
+                            min: '1',
+                            tip: 'Number of partitions'
+                        })
+                    .pc-form-grid-col-60
+                        +form-field__java-class({
+                            label: 'Backup filter',
+                            model: `${affModel}.Fair.affinityBackupFilter`,
+                            name: '"FairAffinityBackupFilter"',
+                            tip: 'Backups will be selected from all nodes that pass this filter'
+                        })
+                    .pc-form-grid-col-60
+                        +form-field__checkbox({
+                            label: 'Exclude neighbors',
+                            model: `${affModel}.Fair.excludeNeighbors`,
+                            name: '"FairExcludeNeighbors"',
+                            tip: 'Exclude same - host - neighbors from being backups of each other and specified number of backups'
+                        })
+                .pc-form-grid-row(ng-if=customAff)
+                    .pc-form-grid-col-60
+                        +form-field__java-class({
+                            label: 'Class name:',
+                            model: `${affModel}.Custom.className`,
+                            name: '"AffCustomClassName"',
+                            required: customAff,
+                            tip: 'Custom key affinity function implementation class name'
+                        })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Mapper:',
+                    model: `${model}.affinityMapper`,
+                    name: '"AffMapCustomClassName"',
+                    tip: 'Provide custom affinity key for any given key'
+                })
+
+            //- Since ignite 2.0
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
+                +form-field__java-class({
+                    label: 'Topology validator:',
+                    model: `${model}.topologyValidator`,
+                    name: '"topologyValidator"'
+                })
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheAffinity')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/concurrency.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/concurrency.pug
new file mode 100644
index 0000000..425c2e5
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/concurrency.pug
@@ -0,0 +1,87 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'concurrency'
+-var model = '$ctrl.clonedCache'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Concurrency control
+    panel-description
+        | Cache concurrent asynchronous operations settings.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Max async operations:',
+                    model: `${model}.maxConcurrentAsyncOperations`,
+                    name: '"maxConcurrentAsyncOperations"',
+                    placeholder: '500',
+                    min: '0',
+                    tip: 'Maximum number of allowed concurrent asynchronous operations<br/>\
+                         If <b>0</b> then number of concurrent asynchronous operations is unlimited'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Default lock timeout:',
+                    model: `${model}.defaultLockTimeout`,
+                    name: '"defaultLockTimeout"',
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Default lock acquisition timeout in milliseconds<br/>\
+                         If <b>0</b> then lock acquisition will never timeout'
+                })
+
+            //- Removed in ignite 2.0
+            .pc-form-grid-col-60(ng-if='$ctrl.available(["1.0.0", "2.0.0"])' ng-hide=`${model}.atomicityMode === 'TRANSACTIONAL'`)
+                +form-field__dropdown({
+                    label: 'Entry versioning:',
+                    model: `${model}.atomicWriteOrderMode`,
+                    name: '"atomicWriteOrderMode"',
+                    placeholder: 'Choose versioning',
+                    options: '[\
+                                            {value: "CLOCK", label: "CLOCK"},\
+                                            {value: "PRIMARY", label: "PRIMARY"}\
+                                        ]',
+                    tip: 'Write ordering mode determines which node assigns the write version, sender or the primary node\
+                                        <ul>\
+                                            <li>CLOCK - in this mode write versions are assigned on a sender node which generally leads to better performance</li>\
+                                            <li>PRIMARY - in this mode version is assigned only on primary node. This means that sender will only send write request to primary node, which in turn will assign write version and forward it to backups</li>\
+                                        </ul>'
+                })
+
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'Write synchronization mode:',
+                    model: `${model}.writeSynchronizationMode`,
+                    name: '"writeSynchronizationMode"',
+                    placeholder: 'PRIMARY_SYNC',
+                    options: '[\
+                                            {value: "FULL_SYNC", label: "FULL_SYNC"},\
+                                            {value: "FULL_ASYNC", label: "FULL_ASYNC"},\
+                                            {value: "PRIMARY_SYNC", label: "PRIMARY_SYNC"}\
+                                        ]',
+                    tip: 'Write synchronization mode\
+                                        <ul>\
+                                            <li>FULL_SYNC - Ignite will wait for write or commit replies from all nodes</li>\
+                                            <li>FULL_ASYNC - Ignite will not wait for write or commit responses from participating nodes</li>\
+                                            <li>PRIMARY_SYNC - Makes sense for PARTITIONED mode. Ignite will wait for write or commit to complete on primary node</li>\
+                                        </ul>'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheConcurrency')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/general.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/general.pug
new file mode 100644
index 0000000..9b875ab
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/general.pug
@@ -0,0 +1,155 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'general'
+-var model = '$ctrl.clonedCache'
+
+panel-collapsible(opened=`::true` ng-form=form)
+    panel-title General
+    panel-description
+        | Common cache configuration.
+        a.link-success(href="https://apacheignite.readme.io/docs/data-grid" target="_blank") More info
+    panel-content.pca-form-row
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'Name:',
+                    model: `${model}.name`,
+                    name: '"cacheName"',
+                    placeholder: 'Input name',
+                    required: true
+                })(
+                    ignite-unique='$ctrl.caches'
+                    ignite-unique-property='name'
+                    ignite-unique-skip=`["_id", ${model}]`
+                )
+                    +form-field__error({ error: 'igniteUnique', message: 'Cache name should be unique' })
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'Domain models:',
+                    model: `${model}.domains`,
+                    name: '"domains"',
+                    multiple: true,
+                    placeholder: 'Choose domain models',
+                    placeholderEmpty: 'No valid domain models configured',
+                    options: '$ctrl.modelsMenu',
+                    tip: 'Select domain models to describe types in cache'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.1.0")')
+                +form-field__text({
+                    label: 'Group:',
+                    model: `${model}.groupName`,
+                    name: '"groupName"',
+                    placeholder: 'Input group name',
+                    tip: 'Cache group name.<br/>\
+                          Caches with the same group name share single underlying "physical" cache (partition set), but are logically isolated.'
+                })
+            .pc-form-grid-col-30
+                +form-field__cache-modes({
+                    label: 'Mode:',
+                    model: `${model}.cacheMode`,
+                    name: '"cacheMode"',
+                    placeholder: 'PARTITIONED'
+                })
+
+            .pc-form-grid-col-30
+                +form-field__dropdown({
+                    label: 'Atomicity:',
+                    model: `${model}.atomicityMode`,
+                    name: '"atomicityMode"',
+                    placeholder: 'ATOMIC',
+                    options: '[\
+                        {value: "ATOMIC", label: "ATOMIC"},\
+                        {value: "TRANSACTIONAL", label: "TRANSACTIONAL"},\
+                        {value: "TRANSACTIONAL_SNAPSHOT", label: "TRANSACTIONAL_SNAPSHOT"}\
+                    ]',
+                    tip: 'Atomicity:\
+                        <ul>\
+                            <li>ATOMIC - in this mode distributed transactions and distributed locking are not supported</li>\
+                            <li>TRANSACTIONAL - in this mode specified fully ACID-compliant transactional cache behavior</li>\
+                            <li>TRANSACTIONAL_SNAPSHOT - in this mode specified fully ACID-compliant transactional cache behavior for both key-value API and SQL transactions</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-30(ng-if=`${model}.cacheMode === 'PARTITIONED'`)
+                +form-field__number({
+                    label: 'Backups:',
+                    model: `${model}.backups`,
+                    name: '"checkpointS3ClientExecutionTimeout"',
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Number of nodes used to back up single partition for partitioned cache'
+                })
+            //- Since ignite 2.0
+            .pc-form-grid-col-30(ng-if='$ctrl.available("2.0.0")')
+                +form-field__dropdown({
+                    label:'Partition loss policy:',
+                    model: `${model}.partitionLossPolicy`,
+                    name: '"partitionLossPolicy"',
+                    placeholder: 'IGNORE',
+                    options: '[\
+                        {value: "READ_ONLY_SAFE", label: "READ_ONLY_SAFE"},\
+                        {value: "READ_ONLY_ALL", label: "READ_ONLY_ALL"},\
+                        {value: "READ_WRITE_SAFE", label: "READ_WRITE_SAFE"},\
+                        {value: "READ_WRITE_ALL", label: "READ_WRITE_ALL"},\
+                        {value: "IGNORE", label: "IGNORE"}\
+                    ]',
+                    tip: 'Partition loss policies:\
+                        <ul>\
+                            <li>READ_ONLY_SAFE - in this mode all writes to the cache will be failed with an exception,\
+                                reads will only be allowed for keys in  non-lost partitions.\
+                                Reads from lost partitions will be failed with an exception.</li>\
+                            <li>READ_ONLY_ALL - in this mode all writes to the cache will be failed with an exception.\
+                                All reads will proceed as if all partitions were in a consistent state.\
+                                The result of reading from a lost partition is undefined and may be different on different nodes in the cluster.</li>\
+                            <li>READ_WRITE_SAFE - in this mode all reads and writes will be allowed for keys in valid partitions.\
+                                All reads and writes for keys in lost partitions will be failed with an exception.</li>\
+                            <li>READ_WRITE_ALL - in this mode all reads and writes will proceed as if all partitions were in a consistent state.\
+                                The result of reading from a lost partition is undefined and may be different on different nodes in the cluster.</li>\
+                            <li>IGNORE - in this mode if partition is lost, reset it state and do not clear intermediate data.\
+                                The result of reading from a previously lost and not cleared partition is undefined and may be different\
+                                on different nodes in the cluster.</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-60(ng-show=`${model}.cacheMode === 'PARTITIONED' && ${model}.backups`)
+                +form-field__checkbox({
+                    label: 'Read from backup',
+                    model: `${model}.readFromBackup`,
+                    name: '"readFromBackup"',
+                    tip: 'Flag indicating whether data can be read from backup<br/>\
+                          If not set then always get data from primary node (never from backup)'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Copy on read',
+                    model: `${model}.copyOnRead`,
+                    name: '"copyOnRead"',
+                    tip: 'Flag indicating whether copy of the value stored in cache should be created for cache operation implying return value<br/>\
+                          Also if this flag is set copies are created for values passed to CacheInterceptor and to CacheEntryProcessor'
+                })
+            .pc-form-grid-col-60(ng-show=`${model}.cacheMode === 'PARTITIONED' && ${model}.atomicityMode === 'TRANSACTIONAL'`)
+                +form-field__checkbox({
+                    label: 'Invalidate near cache',
+                    model: `${model}.isInvalidate`,
+                    name: '"isInvalidate"',
+                    tip: 'Invalidation flag for near cache entries in transaction<br/>\
+                          If set then values will be invalidated (nullified) upon commit in near cache'
+                })
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheGeneral')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/key-cfg.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/key-cfg.pug
new file mode 100644
index 0000000..4527906
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/key-cfg.pug
@@ -0,0 +1,66 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'cacheKeyCfg'
+-var model = '$ctrl.clonedCache.keyConfiguration'
+
+panel-collapsible(ng-show='$ctrl.available("2.1.0")' ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Key configuration
+    panel-description
+        | Configuration defining various aspects of cache keys without explicit usage of annotations on user classes.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6
+            .ignite-form-field
+                +form-field__label({ label: 'Key configuration:', name: '"KeyConfiguration"' })
+
+                list-editable.pc-list-editable-with-form-grid(ng-model=model name='keyConfiguration')
+                    list-editable-item-edit.pc-form-grid-row
+                        - form = '$parent.form'
+                        .pc-form-grid-col-60
+                            +form-field__java-class({
+                                label: 'Type name:',
+                                model: '$item.typeName',
+                                name: '"keyTypeName"',
+                                required: 'true',
+                                tip: 'Type name'
+                            })(
+                                ignite-form-field-input-autofocus='true'
+                                ignite-unique=model
+                                ignite-unique-property='typeName'
+                            )
+                                +form-field__error({ error: 'igniteUnique', message: 'Type name should be unique.' })
+                        .pc-form-grid-col-60
+                            +form-field__text({
+                                label: 'Affinity key field name:',
+                                model: '$item.affinityKeyFieldName',
+                                name: '"affinityKeyFieldName"',
+                                placeholder: 'Enter field name',
+                                tip: 'Affinity key field name',
+                                required: true
+                            })
+
+                    list-editable-no-items
+                        list-editable-add-item-button(
+                            add-item=`(${model} = ${model} || []).push({})`
+                            label-single='configuration'
+                            label-multiple='configurations'
+                        )
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheKeyConfiguration')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/memory.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/memory.pug
new file mode 100644
index 0000000..cf69d65
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/memory.pug
@@ -0,0 +1,252 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'memory'
+-var model = '$ctrl.clonedCache'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Memory
+    panel-description
+        | Cache memory settings.
+        a.link-success(
+            href="https://apacheignite.readme.io/v1.9/docs/off-heap-memory"
+            target="_blank"
+            ng-show='$ctrl.available(["1.0.0", "2.0.0"])'
+        ) More info
+        a.link-success(
+            href="https://apacheignite.readme.io/docs/evictions"
+            target="_blank"
+            ng-show='$ctrl.available("2.0.0")'
+        ) More info
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            //- Since ignite 2.0
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
+                +form-field__checkbox({
+                    label: 'Onheap cache enabled',
+                    model: model + '.onheapCacheEnabled',
+                    name: '"OnheapCacheEnabled"',
+                    tip: 'Checks if the on-heap cache is enabled for the off-heap based page memory'
+                })
+            //- Since ignite 2.0 deprecated in ignite 2.3
+            .pc-form-grid-col-60(ng-if='$ctrl.available(["2.0.0", "2.3.0"])')
+                +form-field__text({
+                    label: 'Memory policy name:',
+                    model: `${model}.memoryPolicyName`,
+                    name: '"MemoryPolicyName"',
+                    placeholder: 'default',
+                    tip: 'Name of memory policy configuration for this cache'
+                })
+
+            //- Since ignite 2.3
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.3.0")')
+                +form-field__text({
+                    label: 'Data region name:',
+                    model: `${model}.dataRegionName`,
+                    name: '"DataRegionName"',
+                    placeholder: 'default',
+                    tip: 'Name of data region configuration for this cache'
+                })
+
+            //- Since ignite 2.8
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.8.0")')
+                +form-field__dropdown({
+                    label: 'Disk page compression:',
+                    model: `${model}.diskPageCompression`,
+                    name: '"diskPageCompression"',
+                    placeholder: 'IGNITE_DEFAULT_DISK_PAGE_COMPRESSION',
+                    options: '::$ctrl.Caches.diskPageCompression',
+                    tip: `Memory modes control whether value is stored in on-heap memory, off-heap memory, or swap space
+                        <ul>
+                            <li>SKIP_GARBAGE - retain only useful data from half-filled pages, but do not apply any compression</li>
+                            <li>ZSTD - Zstd compression<br/></li>
+                            <li>LZ4 - LZ4 compression<br/></li>
+                            <li>SNAPPY - Snappy compression<br/></li>
+                        </ul>`
+                })
+            .pc-form-grid-col-60(ng-if=`$ctrl.available("2.8.0") && ${model}.diskPageCompression === "ZSTD"`)
+                +form-field__number({
+                    label: 'Disk page compression level:',
+                    model: `${model}.diskPageCompressionLevel`,
+                    name: '"diskPageCompressionLevel"',
+                    placeholder: '3',
+                    min: -131072,
+                    max: 22,
+                    tip: 'Disk page compression level from -131072 to 22 or empty to use default 3.<br/>'
+                })
+            .pc-form-grid-col-60(ng-if=`$ctrl.available("2.8.0") && ${model}.diskPageCompression === "LZ4"`)
+                +form-field__number({
+                    label: 'Disk page compression level:',
+                    model: `${model}.diskPageCompressionLevel`,
+                    name: '"diskPageCompressionLevel"',
+                    placeholder: '0',
+                    min: 0,
+                    max: 17,
+                    tip: 'Disk page compression level from 0 to 17 or empty to use default 0.<br/>'
+                })
+
+            //- Removed in ignite 2.0
+            .pc-form-grid-col-60(ng-if-start='$ctrl.available(["1.0.0", "2.0.0"])')
+                +form-field__dropdown({
+                    label: 'Mode:',
+                    model: `${model}.memoryMode`,
+                    name: '"memoryMode"',
+                    placeholder: '{{ ::$ctrl.Caches.memoryMode.default }}',
+                    options: '::$ctrl.Caches.memoryModes',
+                    tip: `Memory modes control whether value is stored in on-heap memory, off-heap memory, or swap space
+                    <ul>
+                        <li>
+                            ONHEAP_TIERED - entries are cached on heap memory first<br/>
+                            <ul>
+                                <li>
+                                    If offheap memory is enabled and eviction policy evicts an entry from heap memory, entry will be moved to offheap memory<br/>
+                                    If offheap memory is disabled, then entry is simply discarded
+                                </li>
+                                <li>
+                                    If swap space is enabled and offheap memory fills up, then entry will be evicted into swap space<br/>
+                                    If swap space is disabled, then entry will be discarded. If swap is enabled and offheap memory is disabled, then entry will be evicted directly from heap memory into swap
+                                </li>
+                            </ul>
+                        </li>
+                        <li>
+                            OFFHEAP_TIERED - works the same as ONHEAP_TIERED, except that entries never end up in heap memory and get stored in offheap memory right away<br/>
+                            Entries get cached in offheap memory first and then get evicted to swap, if one is configured
+                        </li>
+                        <li>
+                            OFFHEAP_VALUES - entry keys will be stored on heap memory, and values will be stored in offheap memory<br/>
+                            Note that in this mode entries can be evicted only to swap
+                        </li>
+                    </ul>`
+                })(
+                    ui-validate=`{
+                        offheapAndDomains: '$ctrl.Caches.memoryMode.offheapAndDomains(${model})'
+                    }`
+                    ui-validate-watch=`"${model}.domains.length"`
+                    ng-model-options='{allowInvalid: true}'
+                )
+                    +form-field__error({ error: 'offheapAndDomains', message: 'Query indexing could not be enabled while values are stored off-heap' })
+            .pc-form-grid-col-60(ng-if=`${model}.memoryMode !== 'OFFHEAP_VALUES'`)
+                +form-field__dropdown({
+                    label: 'Off-heap memory:',
+                    model: `${model}.offHeapMode`,
+                    name: '"offHeapMode"',
+                    required: `$ctrl.Caches.offHeapMode.required(${model})`,
+                    placeholder: '{{::$ctrl.Caches.offHeapMode.default}}',
+                    options: '{{::$ctrl.Caches.offHeapModes}}',
+                    tip: `Off-heap storage mode
+                    <ul>
+                        <li>Disabled - Off-heap storage is disabled</li>
+                        <li>Limited - Off-heap storage has limited size</li>
+                        <li>Unlimited - Off-heap storage grow infinitely (it is up to user to properly add and remove entries from cache to ensure that off-heap storage does not grow infinitely)</li>
+                    </ul>`
+                })(
+                    ng-change=`$ctrl.Caches.offHeapMode.onChange(${model})`
+                    ui-validate=`{
+                        offheapDisabled: '$ctrl.Caches.offHeapMode.offheapDisabled(${model})'
+                    }`
+                    ui-validate-watch=`'${model}.memoryMode'`
+                    ng-model-options='{allowInvalid: true}'
+                )
+                    +form-field__error({ error: 'offheapDisabled', message: 'Off-heap storage can\'t be disabled when memory mode is OFFHEAP_TIERED' })
+            .pc-form-grid-col-60(
+                ng-if=`${model}.offHeapMode === 1 && ${model}.memoryMode !== 'OFFHEAP_VALUES'`
+                ng-if-end
+            )
+                form-field-size(
+                    label='Off-heap memory max size:'
+                    ng-model=`${model}.offHeapMaxMemory`
+                    name='offHeapMaxMemory'
+                    placeholder='Enter off-heap memory size'
+                    min='{{ ::$ctrl.Caches.offHeapMaxMemory.min }}'
+                    tip='Maximum amount of memory available to off-heap storage'
+                    size-scale-label='mb'
+                    size-type='bytes'
+                    required='true'
+                )
+
+            +form-field__eviction-policy({
+                model: `${model}.evictionPolicy`,
+                name: '"evictionPolicy"',
+                enabled: 'true',
+                required: `$ctrl.Caches.evictionPolicy.required(${model})`,
+                tip: 'Optional cache eviction policy<br/>\
+                      Must be set for entries to be evicted from on-heap to off-heap or swap\
+                      <ul>\
+                          <li>Least Recently Used(LRU) - Eviction policy based on LRU algorithm and supports batch eviction</li>\
+                          <li>First In First Out (FIFO) - Eviction policy based on FIFO algorithm and supports batch eviction</li>\
+                          <li>SORTED - Eviction policy which will select the minimum cache entry for eviction</li>\
+                      </ul>'
+            })
+
+            //- Since ignite 2.0
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
+                +form-field__java-class({
+                    label: 'Eviction filter:',
+                    model: `${model}.evictionFilter`,
+                    name: '"EvictionFilter"',
+                    tip: 'Eviction filter to specify which entries should not be evicted'
+                })
+
+            //- Removed in ignite 2.0
+            .pc-form-grid-col-60(ng-if-start='$ctrl.available(["1.0.0", "2.0.0"])')
+                +form-field__number({
+                    label: 'Start size:',
+                    model: `${model}.startSize`,
+                    name: '"startSize"',
+                    placeholder: '1500000',
+                    min: '0',
+                    tip: 'In terms of size and capacity, Ignite internal cache map acts exactly like a normal Java HashMap: it has some initial capacity\
+                          (which is pretty small by default), which doubles as data arrives. The process of internal cache map resizing is CPU-intensive\
+                          and time-consuming, and if you load a huge dataset into cache (which is a normal use case), the map will have to resize a lot of times.\
+                          To avoid that, you can specify the initial cache map capacity, comparable to the expected size of your dataset.\
+                          This will save a lot of CPU resources during the load time, because the map would not have to resize.\
+                          For example, if you expect to load 10 million entries into cache, you can set this property to 10 000 000.\
+                          This will save you from cache internal map resizes.'
+                })
+            .pc-form-grid-col-60(ng-if-end)
+                +form-field__checkbox({
+                    label: 'Swap enabled',
+                    model: `${model}.swapEnabled`,
+                    name: '"swapEnabled"',
+                    tip: 'Flag indicating whether swap storage is enabled or not for this cache'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Cache writer factory:',
+                    model: `${model}.cacheWriterFactory`,
+                    name: `"CacheWriterFactory"`,
+                    tip: 'Factory for writer that is used for write-through to an external resource'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Cache loader factory:',
+                    model: `${model}.cacheLoaderFactory`,
+                    name: `"CacheLoaderFactory"`,
+                    tip: 'Factory for loader that is used for a cache is read-through or when loading data into a cache via the Cache.loadAll(java.util.Set, boolean, CompletionListener) method'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Expiry policy factory:',
+                    model: `${model}.expiryPolicyFactory`,
+                    name: `"ExpiryPolicyFactory"`,
+                    tip: 'Factory for functions that determine when cache entries will expire based on creation, access and modification operations'
+                })
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheMemory')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/misc.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/misc.pug
new file mode 100644
index 0000000..8267f1c
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/misc.pug
@@ -0,0 +1,95 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'misc'
+-var model = '$ctrl.clonedCache'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Miscellaneous
+    panel-description Various miscellaneous cache settings.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Interceptor:',
+                    model: `${model}.interceptor`,
+                    name: '"interceptor"',
+                    tip: 'Cache interceptor can be used for getting callbacks before and after cache get, put, and remove operations'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
+                +form-field__checkbox({
+                    label: 'Store by value',
+                    model: `${model}.storeByValue`,
+                    name: '"storeByValue"',
+                    tip: 'Use store-by-value or store-by-reference semantics'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Eager TTL',
+                    model: `${model}.eagerTtl`,
+                    name: '"eagerTtl"',
+                    tip: 'Eagerly remove expired cache entries'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.7.0")')
+                +form-field__checkbox({
+                    label: 'Enable encryption',
+                    model: `${model}.encryptionEnabled`,
+                    name: '"encryptionEnabled"',
+                    tip: 'Enable encription of data on the disk'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.5.0")')
+                +form-field__checkbox({
+                    label: 'Disable events',
+                    model: `${model}.eventsDisabled`,
+                    name: '"eventsDisabled"',
+                    tip: 'Disable events on this cache'
+                })
+            .pc-form-grid-col-60
+                mixin store-session-listener-factories()
+                    .ignite-form-field
+                        -let items = `${model}.cacheStoreSessionListenerFactories`;
+
+                        list-editable(
+                        ng-model=items
+                        list-editable-cols=`::[{
+                            name: 'Store session listener factories:',
+                            tip: 'Cache store session listener factories'
+                        }]`)
+                            list-editable-item-view {{ $item }}
+
+                            list-editable-item-edit
+                                +list-java-class-field('Listener', '$item', '"Listener"', items)
+                                    +form-field__error({
+                                        error: 'igniteUnique',
+                                        message: 'Listener with such class name already exists!'
+                                    })
+
+                            list-editable-no-items
+                                list-editable-add-item-button(
+                                    add-item=`$editLast((${items} = ${items} || []).push(""))`
+                                    label-single='listener'
+                                    label-multiple='listeners'
+                                )
+
+                - var form = '$parent.form'
+                +store-session-listener-factories
+                - var form = 'misc'
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheMisc')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/near-cache-client.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/near-cache-client.pug
new file mode 100644
index 0000000..a769d1c
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/near-cache-client.pug
@@ -0,0 +1,67 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'clientNearCache'
+-var model = '$ctrl.clonedCache'
+
+panel-collapsible(
+    ng-form=form
+    on-open=`ui.loadPanel('${form}')`
+    ng-show=`${model}.cacheMode === 'PARTITIONED'`
+)
+    panel-title Near cache on client node
+    panel-description
+        | Near cache settings for client nodes.
+        | Near cache is a small local cache that stores most recently or most frequently accessed data.
+        | Should be used in case when it is impossible to send computations to remote nodes.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            -var nearCfg = `${model}.clientNearConfiguration`
+            -var enabled = `${nearCfg}.enabled`
+
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enabled',
+                    model: enabled,
+                    name: '"clientNearEnabled"',
+                    tip: 'Flag indicating whether to configure near cache'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Start size:',
+                    model: `${nearCfg}.nearStartSize`,
+                    name: '"clientNearStartSize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '375000',
+                    min: '0',
+                    tip: 'Initial cache size for near cache which will be used to pre-create internal hash table after start'
+                })
+            +form-field__eviction-policy({
+                model: `${nearCfg}.nearEvictionPolicy`,
+                name: '"clientNearCacheEvictionPolicy"',
+                enabled: enabled,
+                tip: 'Near cache eviction policy\
+                     <ul>\
+                         <li>Least Recently Used (LRU) - Eviction policy based on LRU algorithm and supports batch eviction</li>\
+                         <li>First In First Out (FIFO) - Eviction policy based on FIFO algorithm and supports batch eviction</li>\
+                         <li>SORTED - Eviction policy which will select the minimum cache entry for eviction</li>\
+                     </ul>'
+            })
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheNearClient')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/near-cache-server.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/near-cache-server.pug
new file mode 100644
index 0000000..d0da1d9
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/near-cache-server.pug
@@ -0,0 +1,68 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'serverNearCache'
+-var model = '$ctrl.clonedCache'
+
+panel-collapsible(
+    ng-form=form
+    on-open=`ui.loadPanel('${form}')`
+    ng-show=`${model}.cacheMode === 'PARTITIONED'`
+)
+    panel-title Near cache on server node
+    panel-description
+        | Near cache settings.
+        | Near cache is a small local cache that stores most recently or most frequently accessed data.
+        | Should be used in case when it is impossible to send computations to remote nodes.
+        a.link-success(href="https://apacheignite.readme.io/docs/near-caches" target="_blank") More info
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            -var nearCfg = `${model}.nearConfiguration`
+            -var enabled = `${nearCfg}.enabled`
+
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enabled',
+                    model: enabled,
+                    name: '"nearCacheEnabled"',
+                    tip: 'Flag indicating whether to configure near cache'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Start size:',
+                    model: `${nearCfg}.nearStartSize`,
+                    name: '"nearStartSize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '375000',
+                    min: '0',
+                    tip: 'Initial cache size for near cache which will be used to pre-create internal hash table after start'
+                })
+            +form-field__eviction-policy({
+                model: `${model}.nearConfiguration.nearEvictionPolicy`,
+                name: '"nearCacheEvictionPolicy"',
+                enabled: enabled,
+                tip: 'Near cache eviction policy\
+                     <ul>\
+                         <li>Least Recently Used (LRU) - Eviction policy based on LRU algorithm and supports batch eviction</li>\
+                         <li>First In First Out (FIFO) - Eviction policy based on FIFO algorithm and supports batch eviction</li>\
+                         <li>SORTED - Eviction policy which will select the minimum cache entry for eviction</li>\
+                     </ul>'
+            })
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheNearServer')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/node-filter.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/node-filter.pug
new file mode 100644
index 0000000..0aa5b83
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/node-filter.pug
@@ -0,0 +1,67 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'nodeFilter'
+-var model = '$ctrl.clonedCache'
+-var nodeFilter = model + '.nodeFilter';
+-var nodeFilterKind = nodeFilter + '.kind';
+-var igfsFilter = nodeFilterKind + ' === "IGFS"'
+-var customFilter = nodeFilterKind + ' === "Custom"'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Node filter
+    panel-description Determines on what nodes the cache should be started.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'Node filter:',
+                    model: nodeFilterKind,
+                    name: '"nodeFilter"',
+                    placeholder: 'Not set',
+                    options: '::$ctrl.Caches.nodeFilterKinds',
+                    tip: 'Node filter variant'
+                })
+            .pc-form-grid-col-60(
+                ng-if=igfsFilter
+            )
+                +form-field__dropdown({
+                    label: 'IGFS:',
+                    model: `${nodeFilter}.IGFS.igfs`,
+                    name: '"igfsNodeFilter"',
+                    required: true,
+                    placeholder: 'Choose IGFS',
+                    placeholderEmpty: 'No IGFS configured',
+                    options: '$ctrl.igfssMenu',
+                    tip: 'Select IGFS to filter nodes'
+                })(
+                    pc-is-in-collection='$ctrl.igfsIDs'
+                )
+                    +form-field__error({ error: 'isInCollection',  message: `Cluster doesn't have such an IGFS` })
+            .pc-form-grid-col-60(ng-show=customFilter)
+                +form-field__java-class({
+                    label: 'Class name:',
+                    model: `${nodeFilter}.Custom.className`,
+                    name: '"customNodeFilter"',
+                    required: customFilter,
+                    tip: 'Class name of custom node filter implementation',
+                    validationActive: customFilter
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheNodeFilter', 'igfss')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/query.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/query.pug
new file mode 100644
index 0000000..edbcb25
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/query.pug
@@ -0,0 +1,177 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'query'
+-var model = '$ctrl.clonedCache'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Queries & Indexing
+    panel-description
+        | Cache queries settings.
+        a.link-success(href="https://apacheignite-sql.readme.io/docs/select" target="_blank") More info
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'SQL schema name:',
+                    model: `${model}.sqlSchema`,
+                    name: '"sqlSchema"',
+                    placeholder: 'Input schema name',
+                    tip: 'Cache group name.<br/>\
+                          Caches with the same group name share single underlying "physical" cache (partition set), but are logically isolated.'
+                })
+
+            //- Removed in ignite 2.0
+            .pc-form-grid-col-60(ng-if='$ctrl.available(["1.0.0", "2.0.0"])')
+                +form-field__number({
+                    label: 'On-heap cache for off-heap indexes:',
+                    model: `${model}.sqlOnheapRowCacheSize`,
+                    name: '"sqlOnheapRowCacheSize"',
+                    placeholder: '10240',
+                    min: '1',
+                    tip: 'Specify any custom name to be used as SQL schema for current cache. This name will correspond to SQL ANSI-99 standard.\
+                          Nonquoted identifiers are not case sensitive. Quoted identifiers are case sensitive.\
+                          When SQL schema is not specified, quoted cache name should used instead.<br/>\
+                          For example:\
+                          <ul>\
+                            <li>\
+                            Query without schema names (quoted cache names will be used):\
+                            SELECT * FROM "PersonsCache".Person p INNER JOIN "OrganizationsCache".Organization o on p.org = o.id\
+                            </li>\
+                            <li>\
+                                The same query using schema names "Persons" and "Organizations":\
+                                SELECT * FROM Persons.Person p INNER JOIN Organizations.Organization o on p.org = o.id\
+                            </li>\
+                          </ul>'
+                })
+
+            //- Deprecated in ignite 2.1
+            .pc-form-grid-col-60(ng-if='$ctrl.available(["1.0.0", "2.1.0"])')
+                +form-field__number({
+                    label: 'Long query timeout:',
+                    model: `${model}.longQueryWarningTimeout`,
+                    name: '"longQueryWarningTimeout"',
+                    placeholder: '3000',
+                    min: '0',
+                    tip: 'Timeout in milliseconds after which long query warning will be printed'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'History size:',
+                    model: `${model}.queryDetailMetricsSize`,
+                    name: '"queryDetailMetricsSize"',
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Size of queries detail metrics that will be stored in memory for monitoring purposes'
+                })
+            .pc-form-grid-col-60
+                mixin caches-query-list-sql-functions()
+                    .ignite-form-field
+                        -let items = `${model}.sqlFunctionClasses`;
+
+                        list-editable(
+                            ng-model=items
+                            list-editable-cols=`::[{
+                                name: 'SQL functions:',
+                                tip: 'Collections of classes with user-defined functions for SQL queries'
+                            }]`
+                        )
+                            list-editable-item-view {{ $item }}
+
+                            list-editable-item-edit
+                                +list-java-class-field('SQL function', '$item', '"sqlFunction"', items)
+                                    +form-field__error({ error: 'igniteUnique', message: 'SQL function with such class name already exists!' })
+
+                            list-editable-no-items
+                                list-editable-add-item-button(
+                                    add-item=`$editLast((${items} = ${items} || []).push(""))`
+                                    label-single='SQL function'
+                                    label-multiple='SQL functions'
+                                )
+
+                - var form = '$parent.form'
+                +caches-query-list-sql-functions
+                - var form = 'query'
+
+            //- Removed in ignite 2.0
+            .pc-form-grid-col-60(ng-if='$ctrl.available(["1.0.0", "2.0.0"])')
+                +form-field__checkbox({
+                    label: 'Snapshotable index',
+                    model: `${model}.snapshotableIndex`,
+                    name: '"snapshotableIndex"',
+                    tip: 'Flag indicating whether SQL indexes should support snapshots'
+                })
+
+            //- Since ignite 2.0
+            .pc-form-grid-col-30(ng-if-start='$ctrl.available("2.0.0")')
+                +form-field__number({
+                    label: 'Query parallelism',
+                    model: `${model}.queryParallelism`,
+                    name: '"queryParallelism"',
+                    placeholder: '1',
+                    min: '1',
+                    tip: 'A hint to query execution engine on desired degree of parallelism within a single node'
+                })
+            .pc-form-grid-col-30(ng-if-end)
+                +form-field__number({
+                    label: 'SQL index max inline size:',
+                    model: `${model}.sqlIndexMaxInlineSize`,
+                    name: '"sqlIndexMaxInlineSize"',
+                    placeholder: '-1',
+                    min: '-1',
+                    tip: 'Maximum inline size for sql indexes'
+                })
+            .pc-form-grid-col-60(ng-if-start='$ctrl.available("2.4.0")')
+                +form-field__checkbox({
+                    label: 'Onheap cache enabled',
+                    model: `${model}.sqlOnheapCacheEnabled`,
+                    name: '"sqlOnheapCacheEnabled"',
+                    tip: 'When enabled, Ignite will cache SQL rows as they are accessed by query engine'
+                })
+            .pc-form-grid-col-60(ng-if-end)
+                +form-field__number({
+                    label: 'Onheap cache max size:',
+                    model: `${model}.sqlOnheapCacheMaxSize`,
+                    name: '"SqlOnheapCacheMaxSize"',
+                    disabled: `!${model}.sqlOnheapCacheEnabled`,
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Maximum SQL on-heap cache'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Max query iterators count:',
+                    model: `${model}.maxQueryIteratorsCount`,
+                    name: '"MaxQueryIteratorsCount"',
+                    placeholder: '1024',
+                    min: '1',
+                    tip: 'Maximum number of query iterators that can be stored'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Escape table and filed names',
+                    model: `${model}.sqlEscapeAll`,
+                    name: '"sqlEscapeAll"',
+                    tip: 'If enabled than all schema, table and field names will be escaped with double quotes (for example: "tableName"."fieldName").<br/>\
+                         This enforces case sensitivity for field names and also allows having special characters in table and field names.<br/>\
+                         Escaped names will be used for creation internal structures in Ignite SQL engine.'
+                })
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheQuery', 'domains')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/rebalance.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/rebalance.pug
new file mode 100644
index 0000000..efe0e5f
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/rebalance.pug
@@ -0,0 +1,108 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'rebalance'
+-var model = '$ctrl.clonedCache'
+
+panel-collapsible(
+    ng-form=form
+    on-open=`ui.loadPanel('${form}')`
+    ng-hide=`${model}.cacheMode === "LOCAL"`
+)
+    panel-title Rebalance
+    panel-description
+        | Cache rebalance settings.
+        a.link-success(href="https://apacheignite.readme.io/docs/rebalancing" target="_blank") More info
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-30
+                +form-field__dropdown({
+                    label: 'Mode:',
+                    model: `${model}.rebalanceMode`,
+                    name: '"rebalanceMode"',
+                    placeholder: 'ASYNC',
+                    options: '[\
+                        {value: "SYNC", label: "SYNC"},\
+                        {value: "ASYNC", label: "ASYNC"},\
+                        {value: "NONE", label: "NONE"}\
+                    ]',
+                    tip: 'Rebalance modes\
+                        <ul>\
+                            <li>Synchronous - in this mode distributed caches will not start until all necessary data is loaded from other available grid nodes</li>\
+                            <li>Asynchronous - in this mode distributed caches will start immediately and will load all necessary data from other available grid nodes in the background</li>\
+                            <li>None - in this mode no rebalancing will take place which means that caches will be either loaded on demand from persistent store whenever data is accessed, or will be populated explicitly</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Batch size:',
+                    model: `${model}.rebalanceBatchSize`,
+                    name: '"rebalanceBatchSize"',
+                    placeholder: '512 * 1024',
+                    min: '1',
+                    tip: 'Size (in bytes) to be loaded within a single rebalance message<br/>\
+                          Rebalancing algorithm will split total data set on every node into multiple batches prior to sending data'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Batches prefetch count:',
+                    model: `${model}.rebalanceBatchesPrefetchCount`,
+                    name: '"rebalanceBatchesPrefetchCount"',
+                    placeholder: '2',
+                    min: '1',
+                    tip: 'Number of batches generated by supply node at rebalancing start'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Order:',
+                    model: `${model}.rebalanceOrder`,
+                    name: '"rebalanceOrder"',
+                    placeholder: '0',
+                    min: 'Number.MIN_SAFE_INTEGER',
+                    tip: 'If cache rebalance order is positive, rebalancing for this cache will be started only when rebalancing for all caches with smaller rebalance order (except caches with rebalance order 0) will be completed'
+                })
+            .pc-form-grid-col-20
+                +form-field__number({
+                    label: 'Delay:',
+                    model: `${model}.rebalanceDelay`,
+                    name: '"rebalanceDelay"',
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Delay in milliseconds upon a node joining or leaving topology (or crash) after which rebalancing should be started automatically'
+                })
+            .pc-form-grid-col-20
+                +form-field__number({
+                    label: 'Timeout:',
+                    model: `${model}.rebalanceTimeout`,
+                    name: '"rebalanceTimeout"',
+                    placeholder: '10000',
+                    min: '0',
+                    tip: 'Rebalance timeout in milliseconds'
+                })
+            .pc-form-grid-col-20
+                +form-field__number({
+                    label: 'Throttle:',
+                    model: `${model}.rebalanceThrottle`,
+                    name: '"rebalanceThrottle"',
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Time in milliseconds to wait between rebalance messages to avoid overloading of CPU or network'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheRebalance')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/statistics.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/statistics.pug
new file mode 100644
index 0000000..0270e91
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/statistics.pug
@@ -0,0 +1,44 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'statistics'
+-var model = '$ctrl.clonedCache'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Statistics
+    panel-description Cache statistics and management settings.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Statistics enabled',
+                    model: `${model}.statisticsEnabled`,
+                    name: '"statisticsEnabled"',
+                    tip: 'Flag indicating whether statistics gathering is enabled on this cache'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Management enabled',
+                    model: `${model}.managementEnabled`,
+                    name: '"managementEnabled"',
+                    tip: 'Flag indicating whether management is enabled on this cache<br/>\
+                         If enabled the CacheMXBean for each cache is registered in the platform MBean server'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheStatistics')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/store.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/store.pug
new file mode 100644
index 0000000..42153d8
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cache-edit-form/templates/store.pug
@@ -0,0 +1,432 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'store'
+-var model = '$ctrl.clonedCache'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Store
+    panel-description
+        | Cache store settings.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/3rd-party-store" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            -var storeFactory = `${model}.cacheStoreFactory`;
+            -var storeFactoryKind = `${storeFactory}.kind`;
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'Store factory:',
+                    model: storeFactoryKind,
+                    name: '"cacheStoreFactory"',
+                    placeholder: '{{ ::$ctrl.Caches.cacheStoreFactory.kind.default }}',
+                    options: '::$ctrl.Caches.cacheStoreFactory.values',
+                    tip: `Factory for persistent storage for cache data
+                    <ul>
+                        <li>JDBC POJO store factory - Objects are stored in underlying database by using java beans mapping description via reflection backed by JDBC</li>
+                        <li>JDBC BLOB store factory - Objects are stored in underlying database in BLOB format backed by JDBC</li>
+                        <li>Hibernate BLOB store factory - Objects are stored in underlying database in BLOB format backed by Hibernate</li>
+                    </ul>`
+                })(
+                ui-validate=`{
+                        writeThroughOn: '$ctrl.Caches.cacheStoreFactory.storeDisabledValueOff(${model}, ${model}.writeThrough)',
+                        readThroughOn: '$ctrl.Caches.cacheStoreFactory.storeDisabledValueOff(${model}, ${model}.readThrough)',
+                        writeBehindOn: '$ctrl.Caches.cacheStoreFactory.storeDisabledValueOff(${model}, ${model}.writeBehindEnabled)'
+                    }`
+                ui-validate-watch-collection=`"[${model}.readThrough, ${model}.writeThrough, ${model}.writeBehindEnabled]"`
+                ng-model-options='{allowInvalid: true}'
+                )
+                    +form-field__error({ error: 'writeThroughOn', message: 'Write through is enabled but store is not set' })
+                    +form-field__error({ error: 'readThroughOn', message: 'Read through is enabled but store is not set' })
+                    +form-field__error({ error: 'writeBehindOn', message: 'Write-behind is enabled but store is not set' })
+            .pc-form-group(ng-if=storeFactoryKind)
+                .pc-form-grid-row(ng-if=`${storeFactoryKind} === 'CacheJdbcPojoStoreFactory'`)
+                    -var pojoStoreFactory = `${storeFactory}.CacheJdbcPojoStoreFactory`
+                    -var required = `${storeFactoryKind} === 'CacheJdbcPojoStoreFactory'`
+
+                    .pc-form-grid-col-30
+                        +form-field__text({
+                            label: 'Data source bean name:',
+                            model: `${pojoStoreFactory}.dataSourceBean`,
+                            name: '"pojoDataSourceBean"',
+                            required: required,
+                            placeholder: 'Input bean name',
+                            tip: 'Name of the data source bean in Spring context'
+                        })(
+                            is-valid-java-identifier
+                            not-java-reserved-word
+                        )
+                            +form-field__error({ error: 'required', message: 'Data source bean name is required' })
+                            +form-field__error({ error: 'isValidJavaIdentifier', message: 'Data source bean name is not a valid Java identifier' })
+                            +form-field__error({ error: 'notJavaReservedWord', message: 'Data source bean name should not be a Java reserved word' })
+                    .pc-form-grid-col-30
+                        +form-field__dialect({
+                            label: 'Dialect:',
+                            model: `${pojoStoreFactory}.dialect`,
+                            name: '"pojoDialect"',
+                            required,
+                            tip: 'Dialect of SQL implemented by a particular RDBMS:',
+                            genericDialectName: 'Generic JDBC dialect',
+                            placeholder: 'Choose JDBC dialect',
+                            change:`$ctrl.clearImplementationVersion(${pojoStoreFactory})`
+                        })
+                    .pc-form-grid-col-60(ng-if=`$ctrl.Caches.requiresProprietaryDrivers(${pojoStoreFactory})`)
+                        a.link-success(ng-href=`{{ $ctrl.Caches.jdbcDriverURL(${pojoStoreFactory}) }}` target='_blank')
+                            | Download JDBC drivers?
+                    .pc-form-grid-col-30
+                        +form-field__number({
+                            label:'Batch size:',
+                            model: `${pojoStoreFactory}.batchSize`,
+                            name: '"pojoBatchSize"',
+                            placeholder: '512',
+                            min: '1',
+                            tip: 'Maximum batch size for writeAll and deleteAll operations'
+                        })
+                    .pc-form-grid-col-30
+                        +form-field__number({
+                            label: 'Thread count:',
+                            model: `${pojoStoreFactory}.maximumPoolSize`,
+                            name: '"pojoMaximumPoolSize"',
+                            placeholder: 'availableProcessors',
+                            min: '1',
+                            tip: 'Maximum workers thread count.<br/>\
+                                 These threads are responsible for load cache.'
+                        })
+                    .pc-form-grid-col-30
+                        +form-field__number({
+                            label: 'Maximum write attempts:',
+                            model: `${pojoStoreFactory}.maximumWriteAttempts`,
+                            name: '"pojoMaximumWriteAttempts"',
+                            placeholder: '2',
+                            min: '0',
+                            tip: 'Maximum write attempts in case of database error'
+                        })
+                    .pc-form-grid-col-30
+                        +form-field__number({
+                            label: 'Parallel load threshold:',
+                            model: `${pojoStoreFactory}.parallelLoadCacheMinimumThreshold`,
+                            name: '"pojoParallelLoadCacheMinimumThreshold"',
+                            placeholder: '512',
+                            min: '0',
+                            tip: 'Parallel load cache minimum threshold.<br/>\
+                                 If <b>0</b> then load sequentially.'
+                        })
+                    .pc-form-grid-col-60
+                        +form-field__java-class({
+                            label: 'Hasher:',
+                            model: `${pojoStoreFactory}.hasher`,
+                            name: '"pojoHasher"',
+                            tip: 'Hash calculator',
+                            validationActive: required
+                        })
+                    .pc-form-grid-col-60
+                        +form-field__java-class({
+                            label: 'Transformer:',
+                            model: `${pojoStoreFactory}.transformer`,
+                            name: '"pojoTransformer"',
+                            tip: 'Types transformer',
+                            validationActive: required
+                        })
+                    .pc-form-grid-col-60
+                        +form-field__checkbox({
+                            label: 'Escape table and filed names',
+                            model:`${pojoStoreFactory}.sqlEscapeAll`,
+                            name: '"sqlEscapeAll"',
+                            tip: 'If enabled than all schema, table and field names will be escaped with double quotes (for example: "tableName"."fieldName").<br/>\
+                                  This enforces case sensitivity for field names and also allows having special characters in table and field names.<br/>\
+                                  Escaped names will be used for CacheJdbcPojoStore internal SQL queries.'
+                        })
+                .pc-form-grid-row(ng-if=`${storeFactoryKind} === 'CacheJdbcBlobStoreFactory'`)
+                    -var blobStoreFactory = `${storeFactory}.CacheJdbcBlobStoreFactory`
+                    -var blobStoreFactoryVia = `${blobStoreFactory}.connectVia`
+
+                    .pc-form-grid-col-60
+                        +form-field__dropdown({
+                            label: 'Connect via:',
+                            model: blobStoreFactoryVia,
+                            name: '"connectVia"',
+                            placeholder: 'Choose connection method',
+                            options: '[\
+                                                        {value: "URL", label: "URL"},\
+                                                        {value: "DataSource", label: "Data source"}\
+                                                    ]',
+                            tip: 'You can connect to database via:\
+                                                    <ul>\
+                                                        <li>JDBC URL, for example: jdbc:h2:mem:myDatabase</li>\
+                                                        <li>Configured data source</li>\
+                                                    </ul>'
+                        })
+
+                    -var required = `${storeFactoryKind} === 'CacheJdbcBlobStoreFactory' && ${blobStoreFactoryVia} === 'URL'`
+
+                    .pc-form-grid-col-60(ng-if-start=`${blobStoreFactoryVia} === 'URL'`)
+                        +form-field__text({
+                            label: 'Connection URL:',
+                            model: `${blobStoreFactory}.connectionUrl`,
+                            name: '"connectionUrl"',
+                            required: required,
+                            placeholder: 'Input URL',
+                            tip: 'URL for database access, for example: jdbc:h2:mem:myDatabase'
+                        })
+                    .pc-form-grid-col-30
+                        +form-field__text({
+                            label: 'User:',
+                            model: `${blobStoreFactory}.user`,
+                            name: '"user"',
+                            required: required,
+                            placeholder: 'Input user name',
+                            tip: 'User name for database access'
+                        })
+                    .pc-form-grid-col-30(ng-if-end)
+                        .pc-form-grid__text-only-item Password will be generated as stub.
+
+                    -var required = `${storeFactoryKind} === 'CacheJdbcBlobStoreFactory' && ${blobStoreFactoryVia} !== 'URL'`
+
+                    .pc-form-grid-col-30(ng-if-start=`${blobStoreFactoryVia} !== 'URL'`)
+                        +form-field__text({
+                            label: 'Data source bean name:',
+                            model: `${blobStoreFactory}.dataSourceBean`,
+                            name: '"blobDataSourceBean"',
+                            required: required,
+                            placeholder: 'Input bean name',
+                            tip: 'Name of the data source bean in Spring context'
+                        })(
+                        is-valid-java-identifier
+                        not-java-reserved-word
+                        )
+                            +form-field__error({ error: 'required', message: 'Data source bean name is required' })
+                            +form-field__error({ error: 'isValidJavaIdentifier', message: 'Data source bean name is not a valid Java identifier' })
+                            +form-field__error({ error: 'notJavaReservedWord', message: 'Data source bean name should not be a Java reserved word' })
+                    .pc-form-grid-col-30(ng-if-end)
+                        +form-field__dialect({
+                            label: 'Database:',
+                            model: `${blobStoreFactory}.dialect`,
+                            name: '"blobDialect"',
+                            required,
+                            tip: 'Supported databases:',
+                            genericDialectName: 'Generic database',
+                            placeholder: 'Choose database'
+                        })
+                    .pc-form-grid-col-60(ng-if=`$ctrl.Caches.requiresProprietaryDrivers(${blobStoreFactory})`)
+                        a.link-success(ng-href=`{{ $ctrl.Caches.jdbcDriverURL(${blobStoreFactory}) }}` target='_blank')
+                            | Download JDBC drivers?
+                    .pc-form-grid-col-60
+                        +form-field__checkbox({
+                            label: 'Init schema',
+                            model: `${blobStoreFactory}.initSchema`,
+                            name: '"initSchema"',
+                            tip: 'Flag indicating whether DB schema should be initialized by Ignite (default behaviour) or was explicitly created by user'
+                        })
+                    .pc-form-grid-col-60
+                        +form-field__text({
+                            label: 'Create query:',
+                            model: `${blobStoreFactory}.createTableQuery`,
+                            name: '"createTableQuery"',
+                            placeholder: 'SQL for table creation',
+                            tip: 'Query for table creation in underlying database<br/>\
+                                 Default value: create table if not exists ENTRIES (key binary primary key, val binary)'
+                        })
+                    .pc-form-grid-col-60
+                        +form-field__text({
+                            label: 'Load query:',
+                            model: `${blobStoreFactory}.loadQuery`,
+                            name: '"loadQuery"',
+                            placeholder: 'SQL for load entry',
+                            tip: 'Query for entry load from underlying database<br/>\
+                                 Default value: select * from ENTRIES where key=?'
+                        })
+                    .pc-form-grid-col-60
+                        +form-field__text({
+                            label: 'Insert query:',
+                            model: `${blobStoreFactory}.insertQuery`,
+                            name: '"insertQuery"',
+                            placeholder: 'SQL for insert entry',
+                            tip: 'Query for insert entry into underlying database<br/>\
+                                 Default value: insert into ENTRIES (key, val) values (?, ?)'
+                        })
+                    .pc-form-grid-col-60
+                        +form-field__text({
+                            label: 'Update query:',
+                            model: `${blobStoreFactory}.updateQuery`,
+                            name: '"updateQuery"',
+                            placeholder: 'SQL for update entry',
+                            tip: 'Query for update entry in underlying database<br/>\
+                                 Default value: update ENTRIES set val=? where key=?'
+                        })
+                    .pc-form-grid-col-60
+                        +form-field__text({
+                            label: 'Delete query:',
+                            model: `${blobStoreFactory}.deleteQuery`,
+                            name: '"deleteQuery"',
+                            placeholder: 'SQL for delete entry',
+                            tip: 'Query for delete entry from underlying database<br/>\
+                                 Default value: delete from ENTRIES where key=?'
+                        })
+
+                .pc-form-grid-row(ng-if=`${storeFactoryKind} === 'CacheHibernateBlobStoreFactory'`)
+                    -var hibernateStoreFactory = `${storeFactory}.CacheHibernateBlobStoreFactory`
+
+                    .pc-form-grid-col-60
+                        .ignite-form-field
+                            +form-field__label({ label: 'Hibernate properties:', name: '"hibernateProperties"' })
+                                +form-field__tooltip({ title: `List of Hibernate properties<bt />
+                                    For example: connection.url=jdbc:h2:mem:exampleDb` })
+
+                            +list-pair-edit({
+                                items: `${hibernateStoreFactory}.hibernateProperties`,
+                                keyLbl: 'Property name',
+                                valLbl: 'Property value',
+                                itemName: 'property',
+                                itemsName: 'properties'
+                            })
+
+            - form = 'store'
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Concurrent load all threshold:',
+                    model: `${model}.storeConcurrentLoadAllThreshold`,
+                    name: '"storeConcurrentLoadAllThreshold"',
+                    placeholder: '5',
+                    min: '1',
+                    tip: 'Threshold used in cases when values for multiple keys are being loaded from an underlying cache store in parallel'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Keep binary in store',
+                    model: `${model}.storeKeepBinary`,
+                    name: '"storeKeepBinary"',
+                    tip: 'Flag indicating that CacheStore implementation is working with binary objects instead of Java objects'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Load previous value',
+                    model: `${model}.loadPreviousValue`,
+                    name: '"loadPreviousValue"',
+                    tip: 'Flag indicating whether value should be loaded from store if it is not in the cache for following cache operations: \
+                        <ul> \
+                            <li>IgniteCache.putIfAbsent()</li> \
+                            <li>IgniteCache.replace()</li> \
+                            <li>IgniteCache.remove()</li> \
+                            <li>IgniteCache.getAndPut()</li> \
+                            <li>IgniteCache.getAndRemove()</li> \
+                            <li>IgniteCache.getAndReplace()</li> \
+                            <li> IgniteCache.getAndPutIfAbsent()</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Read-through',
+                    model: `${model}.readThrough`,
+                    name: '"readThrough"',
+                    tip: 'Flag indicating whether read-through caching should be used'
+                })(
+                ng-model-options='{allowInvalid: true}'
+                ui-validate=`{
+                        storeEnabledReadOrWriteOn: '$ctrl.Caches.cacheStoreFactory.storeEnabledReadOrWriteOn(${model})'
+                    }`
+                ui-validate-watch-collection=`"[${storeFactoryKind}, ${model}.writeThrough, ${model}.readThrough]"`
+                )
+                    +form-field__error({ error: 'storeEnabledReadOrWriteOn', message: 'Read or write through should be turned on when store kind is set' })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Write-through',
+                    model: `${model}.writeThrough`,
+                    name: '"writeThrough"',
+                    tip: 'Flag indicating whether write-through caching should be used'
+                })(
+                ng-model-options='{allowInvalid: true}'
+                ui-validate=`{
+                        storeEnabledReadOrWriteOn: '$ctrl.Caches.cacheStoreFactory.storeEnabledReadOrWriteOn(${model})'
+                    }`
+                ui-validate-watch-collection=`"[${storeFactoryKind}, ${model}.writeThrough, ${model}.readThrough]"`
+                )
+                    +form-field__error({ error: 'storeEnabledReadOrWriteOn', message: 'Read or write through should be turned on when store kind is set' })
+
+            -var enabled = `${model}.writeBehindEnabled`
+
+            .pc-form-grid-col-60.pc-form-group__text-title
+                +form-field__checkbox({
+                    label: 'Write-behind',
+                    model: enabled,
+                    name: '"writeBehindEnabled"',
+                    tip: `
+                        Cache write-behind settings.<br>
+                        Write-behind is a special mode when updates to cache accumulated and then asynchronously flushed to persistent store as a bulk operation.
+                    `
+                })(
+                ng-model-options='{allowInvalid: true}'
+                )
+                    +form-field__error({ error: 'storeDisabledValueOff', message: 'Write-behind is enabled but store kind is not set' })
+            .pc-form-group.pc-form-grid-row(ng-if=enabled)
+                .pc-form-grid-col-30
+                    +form-field__number({
+                        label: 'Batch size:',
+                        model: `${model}.writeBehindBatchSize`,
+                        name: '"writeBehindBatchSize"',
+                        disabled: `!(${enabled})`,
+                        placeholder: '512',
+                        min: '1',
+                        tip: 'Maximum batch size for write-behind cache store operations<br/>\
+                              Store operations(get or remove) are combined in a batch of this size to be passed to cache store'
+                    })
+                .pc-form-grid-col-30
+                    +form-field__number({
+                        label: 'Flush size:',
+                        model: `${model}.writeBehindFlushSize`,
+                        name: '"writeBehindFlushSize"',
+                        placeholder: '10240',
+                        min: `{{ $ctrl.Caches.writeBehindFlush.min(${model}) }}`,
+                        tip: `Maximum size of the write-behind cache<br/>
+                         If cache size exceeds this value, all cached items are flushed to the cache store and write cache is cleared`
+                    })(
+                    ng-model-options='{allowInvalid: true}'
+                    )
+                .pc-form-grid-col-30
+                    +form-field__number({
+                        label: 'Flush frequency:',
+                        model: `${model}.writeBehindFlushFrequency`,
+                        name: '"writeBehindFlushFrequency"',
+                        placeholder: '5000',
+                        min: `{{ $ctrl.Caches.writeBehindFlush.min(${model}) }}`,
+                        tip: `Frequency with which write-behind cache is flushed to the cache store in milliseconds`
+                    })(
+                    ng-model-options='{allowInvalid: true}'
+                    )
+                .pc-form-grid-col-30
+                    +form-field__number({
+                        label: 'Flush threads count:',
+                        model: `${model}.writeBehindFlushThreadCount`,
+                        name: '"writeBehindFlushThreadCount"',
+                        disabled: `!(${enabled})`,
+                        placeholder: '1',
+                        min: '1',
+                        tip: 'Number of threads that will perform cache flushing'
+                    })
+
+                //- Since ignite 2.0
+                .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
+                    +form-field__checkbox({
+                        label: 'Write coalescing',
+                        model: model + '.writeBehindCoalescing',
+                        name: '"WriteBehindCoalescing"',
+                        disabled: `!${enabled}`,
+                        tip: 'Write coalescing flag for write-behind cache store'
+                    })
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheStore', 'domains')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/component.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/component.ts
new file mode 100644
index 0000000..6df5337
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/component.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import controller from './controller';
+import templateUrl from './template.tpl.pug';
+import './style.scss';
+
+export default {
+    controller,
+    templateUrl,
+    bindings: {
+        isNew: '<',
+        cluster: '<',
+        caches: '<',
+        onSave: '&'
+    }
+};
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/controller.spec.js b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/controller.spec.js
new file mode 100644
index 0000000..cac888f
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/controller.spec.js
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'mocha';
+import {assert} from 'chai';
+import {spy} from 'sinon';
+import Controller from './controller';
+
+suite('cluster-edit-form controller', () => {
+    test('cluster binding changes', () => {
+        const $scope = {
+            ui: {
+                inputForm: {
+                    $setPristine: spy(),
+                    $setUntouched: spy()
+                }
+            }
+        };
+
+        const mocks = Controller.$inject.map((token) => {
+            switch (token) {
+                case '$scope': return $scope;
+                default: return null;
+            }
+        });
+
+        const changeBoundCluster = ($ctrl, cluster) => {
+            $ctrl.cluster = cluster;
+            $ctrl.$onChanges({
+                cluster: {
+                    currentValue: cluster
+                }
+            });
+        };
+
+        const $ctrl = new Controller(...mocks);
+
+        const cluster1 = {_id: 1, caches: [1, 2, 3]};
+        const cluster2 = {_id: 1, caches: [1, 2, 3, 4, 5, 6], models: [1, 2, 3]};
+        const cluster3 = {_id: 1, caches: [1, 2, 3, 4, 5, 6], models: [1, 2, 3], name: 'Foo'};
+
+        changeBoundCluster($ctrl, cluster1);
+
+        assert.notEqual($ctrl.clonedCluster, cluster1, 'Cloned cluster is really cloned');
+        assert.deepEqual($ctrl.clonedCluster, cluster1, 'Cloned cluster is really a clone of incloming value');
+        assert.equal(1, $scope.ui.inputForm.$setPristine.callCount, 'Sets form pristine when cluster value changes');
+        assert.equal(1, $scope.ui.inputForm.$setUntouched.callCount, 'Sets form untouched when cluster value changes');
+
+        changeBoundCluster($ctrl, cluster2);
+
+        assert.deepEqual(
+            $ctrl.clonedCluster,
+            cluster2,
+            'Overrides clonedCluster if incoming cluster has same id but different caches or models'
+        );
+        assert.equal(2, $scope.ui.inputForm.$setPristine.callCount, 'Sets form pristine when bound cluster caches/models change');
+        assert.equal(2, $scope.ui.inputForm.$setUntouched.callCount, 'Sets form untouched when bound cluster caches/models change');
+
+        changeBoundCluster($ctrl, cluster3);
+
+        assert.deepEqual(
+            $ctrl.clonedCluster,
+            cluster2,
+            'Does not change cloned cluster value if fields other than id, chaches and models change'
+        );
+    });
+});
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/controller.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/controller.ts
new file mode 100644
index 0000000..1de84ab
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/controller.ts
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import cloneDeep from 'lodash/cloneDeep';
+import get from 'lodash/get';
+import isEqual from 'lodash/isEqual';
+import _ from 'lodash';
+import {tap} from 'rxjs/operators';
+import {ShortCache} from '../../../../types';
+import {Menu} from 'app/types';
+import Clusters from '../../../../services/Clusters';
+import LegacyUtils from 'app/services/LegacyUtils.service';
+import IgniteEventGroups from '../../../../generator/generator/defaults/Event-groups.service';
+import LegacyConfirm from 'app/services/Confirm.service';
+import Version from 'app/services/Version.service';
+import FormUtils from 'app/services/FormUtils.service';
+
+export default class ClusterEditFormController {
+    caches: ShortCache[];
+    cachesMenu: Menu<string>;
+    servicesCachesMenu: Menu<string>;
+    onSave: ng.ICompiledExpression;
+
+    static $inject = ['IgniteLegacyUtils', 'IgniteEventGroups', 'IgniteConfirm', 'IgniteVersion', '$scope', 'Clusters', 'IgniteFormUtils'];
+
+    constructor(
+        private IgniteLegacyUtils: ReturnType<typeof LegacyUtils>,
+        private IgniteEventGroups: IgniteEventGroups,
+        private IgniteConfirm: ReturnType<typeof LegacyConfirm>,
+        private IgniteVersion: Version,
+        private $scope: ng.IScope,
+        private Clusters: Clusters,
+        private IgniteFormUtils: ReturnType<typeof FormUtils>
+    ) {}
+
+    $onInit() {
+        this.available = this.IgniteVersion.available.bind(this.IgniteVersion);
+
+        const rebuildDropdowns = () => {
+            this.eventStorage = [
+                {value: 'Memory', label: 'Memory'},
+                {value: 'Custom', label: 'Custom'}
+            ];
+
+            this.marshallerVariant = [
+                {value: 'JdkMarshaller', label: 'JdkMarshaller'},
+                {value: null, label: 'Default'}
+            ];
+
+            this.failureHandlerVariant = [
+                {value: 'RestartProcess', label: 'Restart process'},
+                {value: 'StopNodeOnHalt', label: 'Try stop with timeout'},
+                {value: 'StopNode', label: 'Stop on critical error'},
+                {value: 'Noop', label: 'Disabled'},
+                {value: 'Custom', label: 'Custom'},
+                {value: null, label: 'Default'}
+            ];
+
+            this.ignoredFailureTypes = [
+                {value: 'SEGMENTATION', label: 'SEGMENTATION'},
+                {value: 'SYSTEM_WORKER_TERMINATION', label: 'SYSTEM_WORKER_TERMINATION'},
+                {value: 'SYSTEM_WORKER_BLOCKED', label: 'SYSTEM_WORKER_BLOCKED'},
+                {value: 'CRITICAL_ERROR', label: 'CRITICAL_ERROR'},
+                {value: 'SYSTEM_CRITICAL_OPERATION_TIMEOUT', label: 'SYSTEM_CRITICAL_OPERATION_TIMEOUT'}
+            ];
+
+            if (this.available('2.0.0')) {
+                this.eventStorage.push({value: null, label: 'Disabled'});
+
+                this.eventGroups = _.filter(this.IgniteEventGroups, ({value}) => value !== 'EVTS_SWAPSPACE');
+
+                _.forEach(this.eventGroups, (grp) => grp.events = _.filter(grp.events, (evt) => evt.indexOf('SWAP') < 0));
+            }
+            else {
+                this.eventGroups = this.IgniteEventGroups;
+
+                this.marshallerVariant.splice(0, 0, {value: 'OptimizedMarshaller', label: 'OptimizedMarshaller'});
+            }
+
+            this.eventTypes = [];
+
+            _.forEach(this.eventGroups, (grp) => {
+                _.forEach(grp.events, (e) => {
+                    const newVal = {value: e, label: e};
+
+                    if (!_.find(this.eventTypes, newVal))
+                        this.eventTypes.push(newVal);
+                });
+            });
+        };
+
+        rebuildDropdowns();
+
+        const filterModel = (cluster) => {
+            if (cluster) {
+                if (this.available('2.0.0')) {
+                    const evtGrps = _.map(this.eventGroups, 'value');
+
+                    _.remove(cluster.includeEventTypes, (evtGrp) => !_.includes(evtGrps, evtGrp));
+
+                    if (_.get(cluster, 'marshaller.kind') === 'OptimizedMarshaller')
+                        cluster.marshaller.kind = null;
+                }
+                else if (cluster && !_.get(cluster, 'eventStorage.kind'))
+                    _.set(cluster, 'eventStorage.kind', 'Memory');
+            }
+        };
+
+        this.subscription = this.IgniteVersion.currentSbj.pipe(
+            tap(rebuildDropdowns),
+            tap(() => filterModel(this.clonedCluster))
+        )
+        .subscribe();
+
+        this.supportedJdbcTypes = this.IgniteLegacyUtils.mkOptions(this.IgniteLegacyUtils.SUPPORTED_JDBC_TYPES);
+
+        this.$scope.ui = this.IgniteFormUtils.formUI();
+        this.$scope.ui.loadedPanels = ['checkpoint', 'serviceConfiguration', 'odbcConfiguration'];
+
+        this.formActions = [
+            {text: 'Save', icon: 'checkmark', click: () => this.save()},
+            {text: 'Save and Download', icon: 'download', click: () => this.save(true)}
+        ];
+    }
+
+    $onDestroy() {
+        this.subscription.unsubscribe();
+    }
+
+    $onChanges(changes) {
+        if ('cluster' in changes && this.shouldOverwriteValue(this.cluster, this.clonedCluster)) {
+            this.clonedCluster = cloneDeep(changes.cluster.currentValue);
+            if (this.$scope.ui && this.$scope.ui.inputForm) {
+                this.$scope.ui.inputForm.$setPristine();
+                this.$scope.ui.inputForm.$setUntouched();
+            }
+        }
+
+        if ('caches' in changes) {
+            this.cachesMenu = (changes.caches.currentValue || []).map((c) => ({label: c.name, value: c._id}));
+            this.servicesCachesMenu = [{label: 'Key-affinity not used', value: null}].concat(this.cachesMenu);
+        }
+    }
+
+    /**
+     * The form should accept incoming cluster value if:
+     * 1. It has different _id ("new" to real id).
+     * 2. Different caches or models (imported from DB).
+     * @param a Incoming value.
+     * @param b Current value.
+     */
+    shouldOverwriteValue<T>(a: T, b: T) {
+        return get(a, '_id') !== get(b, '_id') ||
+            !isEqual(get(a, 'caches'), get(b, 'caches')) ||
+            !isEqual(get(a, 'models'), get(b, 'models'));
+    }
+
+    getValuesToCompare() {
+        return [this.cluster, this.clonedCluster].map(this.Clusters.normalize);
+    }
+
+    save(download) {
+        if (this.$scope.ui.inputForm.$invalid)
+            return this.IgniteFormUtils.triggerValidation(this.$scope.ui.inputForm, this.$scope);
+
+        this.onSave({$event: {cluster: cloneDeep(this.clonedCluster), download}});
+    }
+
+    reset = () => this.clonedCluster = cloneDeep(this.cluster);
+
+    confirmAndReset() {
+        return this.IgniteConfirm
+            .confirm('Are you sure you want to undo all changes for current cluster?')
+            .then(this.reset);
+    }
+}
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/index.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/index.ts
new file mode 100644
index 0000000..84f7601
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('configuration.cluster-edit-form', [])
+    .component('clusterEditForm', component);
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/style.scss b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/style.scss
new file mode 100644
index 0000000..d656f3d
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/style.scss
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+cache-edit-form {
+    display: block;
+}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/template.tpl.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/template.tpl.pug
new file mode 100644
index 0000000..008fd97
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/template.tpl.pug
@@ -0,0 +1,90 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+form(id='cluster' name='ui.inputForm' novalidate)
+    .panel-group
+        include ./templates/general
+
+        include ./templates/atomic
+        include ./templates/binary
+        include ./templates/cache-key-cfg
+        include ./templates/checkpoint
+
+        //- Since ignite 2.3
+        include ./templates/client-connector
+
+        include ./templates/collision
+        include ./templates/communication
+        include ./templates/connector
+        include ./templates/deployment
+
+        //- Since ignite 2.3
+        include ./templates/data-storage
+
+        include ./templates/discovery
+
+        //- Since ignite 2.7
+        include ./templates/encryption
+
+        include ./templates/events
+        include ./templates/failover
+        include ./templates/hadoop
+        include ./templates/load-balancing
+        include ./templates/logger
+        include ./templates/marshaller
+
+        //- Since ignite 2.0, deprecated in ignite 2.3
+        include ./templates/memory
+
+        include ./templates/misc
+        include ./templates/metrics
+
+        //- Since ignite 2.7
+        include ./templates/mvcc
+
+        //- Deprecated in ignite 2.1
+        include ./templates/odbc
+
+        //- Since ignite 2.1, deprecated in ignite 2.3
+        include ./templates/persistence
+
+        //- Deprecated in ignite 2.3
+        include ./templates/sql-connector
+
+        include ./templates/service
+        include ./templates/ssl
+
+        //- Removed in ignite 2.0
+        include ./templates/swap
+
+        include ./templates/thread
+        include ./templates/time
+        include ./templates/transactions
+        include ./templates/attributes
+
+.pc-form-actions-panel(n_g-show='$ctrl.$scope.selectedItem')
+    button-preview-project(cluster='$ctrl.cluster' ng-hide='$ctrl.isNew')
+
+    .pc-form-actions-panel__right-after
+
+    button.btn-ignite.btn-ignite--link-success(
+        type='button'
+        ng-click='$ctrl.confirmAndReset()'
+    )
+        | Cancel
+    pc-split-button(actions=`::$ctrl.formActions`)
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/atomic.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/atomic.pug
new file mode 100644
index 0000000..b27ada9
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/atomic.pug
@@ -0,0 +1,129 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'atomics'
+-var model = '$ctrl.clonedCluster.atomicConfiguration'
+-var affModel = model + '.affinity'
+-var rendezvousAff = affModel + '.kind === "Rendezvous"'
+-var customAff = affModel + '.kind === "Custom"'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Atomic configuration
+    panel-description
+        | Configuration for atomic data structures.
+        | Atomics are distributed across the cluster, essentially enabling performing atomic operations (such as increment-and-get or compare-and-set) with the same globally-visible value.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/atomic-types" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-30
+                +form-field__dropdown({
+                    label: 'Cache mode:',
+                    model: `${model}.cacheMode`,
+                    name: '"cacheMode"',
+                    placeholder: 'PARTITIONED',
+                    options: '[\
+                        {value: "LOCAL", label: "LOCAL"},\
+                        {value: "REPLICATED", label: "REPLICATED"},\
+                        {value: "PARTITIONED", label: "PARTITIONED"}\
+                    ]',
+                    tip: 'Cache modes:\
+                        <ul>\
+                            <li>Partitioned - in this mode the overall key set will be divided into partitions and all partitions will be split equally between participating nodes</li>\
+                            <li>Replicated - in this mode all the keys are distributed to all participating nodes</li>\
+                            <li>Local - in this mode caches residing on different grid nodes will not know about each other</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Sequence reserve:',
+                    model: `${model}.atomicSequenceReserveSize`,
+                    name: '"atomicSequenceReserveSize"',
+                    placeholder: '1000',
+                    min: '0',
+                    tip: 'Default number of sequence values reserved for IgniteAtomicSequence instances<br/>\
+                          After a certain number has been reserved, consequent increments of sequence will happen locally, without communication with other nodes, until the next reservation has to be made'
+                })
+            .pc-form-grid-col-60(ng-show=`!(${model}.cacheMode && ${model}.cacheMode != "PARTITIONED")`)
+                +form-field__number({
+                    label: 'Backups:',
+                    model: model + '.backups',
+                    name: '"backups"',
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Number of backup nodes'
+                })
+
+            .pc-form-grid-col-60(ng-if-start='$ctrl.available("2.1.0")')
+                +form-field__dropdown({
+                    label: 'Function:',
+                    model: `${affModel}.kind`,
+                    name: '"AffinityKind"',
+                    placeholder: 'Default',
+                    options: '$ctrl.Clusters.affinityFunctions',
+                    tip: 'Key topology resolver to provide mapping from keys to nodes\
+                        <ul>\
+                            <li>Rendezvous - Based on Highest Random Weight algorithm<br/></li>\
+                            <li>Custom - Custom implementation of key affinity function<br/></li>\
+                            <li>Default - By default rendezvous affinity function  with 1024 partitions is used<br/></li>\
+                        </ul>'
+                })
+            .pc-form-group(ng-if-end ng-if=rendezvousAff + ' || ' + customAff)
+                .pc-form-grid-row
+                    .pc-form-grid-col-30(ng-if-start=rendezvousAff)
+                        +form-field__number({
+                            label: 'Partitions',
+                            model: `${affModel}.Rendezvous.partitions`,
+                            name: '"RendPartitions"',
+                            required: rendPartitionsRequired,
+                            placeholder: '1024',
+                            min: '1',
+                            tip: 'Number of partitions'
+                        })
+                    .pc-form-grid-col-30
+                        +form-field__java-class({
+                            label: 'Backup filter',
+                            model: `${affModel}.Rendezvous.affinityBackupFilter`,
+                            name: '"RendAffinityBackupFilter"',
+                            tip: 'Backups will be selected from all nodes that pass this filter'
+                        })
+                    .pc-form-grid-col-60(ng-if-end)
+                        +form-field__checkbox({
+                            label: 'Exclude neighbors',
+                            model: `${affModel}.Rendezvous.excludeNeighbors`,
+                            name: '"RendExcludeNeighbors"',
+                            tip: 'Exclude same - host - neighbors from being backups of each other and specified number of backups'
+                        })
+                    .pc-form-grid-col-60(ng-if=customAff)
+                        +form-field__java-class({
+                            label: 'Class name:',
+                            model: `${affModel}.Custom.className`,
+                            name: '"AffCustomClassName"',
+                            required: customAff,
+                            tip: 'Custom key affinity function implementation class name'
+                        })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.1.0")')
+                +form-field__text({
+                    label: 'Default group name:',
+                    model: `${model}.groupName`,
+                    name: '"AtomicGroupName"',
+                    placeholder: 'Input group name',
+                    tip: 'Group name'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterAtomics')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/attributes.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/attributes.pug
new file mode 100644
index 0000000..17a20f2
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/attributes.pug
@@ -0,0 +1,41 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'attributes'
+-var model = '$ctrl.clonedCluster'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title User attributes
+    panel-description Configuration for Ignite user attributes.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6
+            .ignite-form-field
+                +form-field__label({ label: 'User attributes:', name: '"userAttributes"'})
+                    +form-field__tooltip({ title: `User-defined attributes to add to node` })
+
+                +list-pair-edit({
+                    items: `${model}.attributes`,
+                    keyLbl: 'Attribute name', 
+                    valLbl: 'Attribute value',
+                    itemName: 'attribute',
+                    itemsName: 'attributes'
+                })
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterUserAttributes')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/binary.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/binary.pug
new file mode 100644
index 0000000..07b02a6
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/binary.pug
@@ -0,0 +1,151 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'binary'
+-var model = '$ctrl.clonedCluster.binaryConfiguration'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Binary configuration
+    panel-description
+        | Configuration of specific binary types.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/binary-marshaller" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'ID mapper:',
+                    model: model + '.idMapper',
+                    name: '"idMapper"',
+                    tip: 'Maps given from BinaryNameMapper type and filed name to ID that will be used by Ignite in internals<br/>\
+                          Ignite never writes full strings for field or type names. Instead, for performance reasons, Ignite writes integer hash codes for type/class and field names. It has been tested that hash code conflicts for the type/class names or the field names within the same type are virtually non - existent and, to gain performance, it is safe to work with hash codes. For the cases when hash codes for different types or fields actually do collide <b>BinaryIdMapper</b> allows to override the automatically generated hash code IDs for the type and field names'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Name mapper:',
+                    model: model + '.nameMapper',
+                    name: '"nameMapper"',
+                    tip: 'Maps type/class and field names to different names'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Serializer:',
+                    model: model + '.serializer',
+                    name: '"serializer"',
+                    tip: 'Class with custom serialization logic for binary objects'
+                })
+            .pc-form-grid-col-60
+                .ignite-form-field
+                    +form-field__label({ label: 'Type configurations:', name: '"typeConfigurations"' })
+                        +form-field__tooltip({ title: `Configuration properties for binary types`})
+
+                    -var items = model + '.typeConfigurations'
+                    list-editable.pc-list-editable-with-form-grid(ng-model=items name='typeConfigurations')
+                        list-editable-item-edit.pc-form-grid-row
+                            - form = '$parent.form'
+                            .pc-form-grid-col-60
+                                +form-field__java-class({
+                                    label: 'Type name:',
+                                    model: '$item.typeName',
+                                    name: '"typeName"',
+                                    required: 'true',
+                                    tip: 'Type name'
+                                })(
+                                    ignite-form-field-input-autofocus='true'
+                                    ignite-unique=items
+                                    ignite-unique-property='typeName'
+                                )
+                                    +form-field__error({ error: 'igniteUnique', message: 'Type name should be unique.' })
+                            .pc-form-grid-col-60
+                                +form-field__java-class({
+                                    label: 'ID mapper:',
+                                    model: '$item.idMapper',
+                                    name: '"idMapper"',
+                                    tip: 'Maps given from BinaryNameMapper type and filed name to ID that will be used by Ignite in internals<br/>\
+                                          Ignite never writes full strings for field or type/class names.\
+                                          Instead, for performance reasons, Ignite writes integer hash codes for type/class and field names.\
+                                          It has been tested that hash code conflicts for the type/class names or the field names within the same type are virtually non - existent and,\
+                                          to gain performance, it is safe to work with hash codes.\
+                                          For the cases when hash codes for different types or fields actually do collide <b>BinaryIdMapper</b> allows to override the automatically generated hash code IDs for the type and field names'
+                                })
+                            .pc-form-grid-col-60
+                                +form-field__java-class({
+                                    label: 'Name mapper:',
+                                    model: '$item.nameMapper',
+                                    name: '"nameMapper"',
+                                    tip: 'Maps type/class and field names to different names'
+                                })
+
+                            .pc-form-grid-col-60
+                                +form-field__java-class({
+                                    label: 'Serializer:',
+                                    model: '$item.serializer',
+                                    name: '"serializer"',
+                                    tip: 'Class with custom serialization logic for binary object'
+                                })
+                            .pc-form-grid-col-60
+                                +form-field__checkbox({
+                                    label: 'Enum',
+                                    model: '$item.enum',
+                                    name: 'enum',
+                                    tip: 'Flag indicating that this type is the enum'
+                                })
+                            .pc-form-grid-col-60(ng-if='$item.enum')
+                                mixin enum-values
+                                    .ignite-form-field
+                                        -let items = '$item.enumValues'
+
+                                        list-editable(
+                                            ng-model=items
+                                            name='enumValues'
+                                            list-editable-cols=`::[{name: "Enum values:"}]`
+                                        )
+                                            list-editable-item-view {{ $item }}
+
+                                            list-editable-item-edit
+                                                +list-java-identifier-field('Value', '$item', '"value"', 'Enter Enum value', '$item.$item.enumValues')
+                                                    +form-field__error({error: 'igniteUnique', message: 'Value already configured!'})
+
+                                            list-editable-no-items
+                                                list-editable-add-item-button(
+                                                    add-item=`$editLast((${items} = ${items} || []).push(''))`
+                                                    label-single='enum value'
+                                                    label-multiple='enum values'
+                                                )
+
+                                - var form = '$parent.$parent.form'
+                                +enum-values
+                                - var form = '$parent.form'
+
+                        list-editable-no-items
+                            list-editable-add-item-button(
+                                add-item=`$ctrl.Clusters.addBinaryTypeConfiguration($ctrl.clonedCluster)`
+                                label-single='configuration'
+                                label-multiple='configurations'
+                            )
+
+            - form = 'binary'
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Compact footer',
+                    model: model + '.compactFooter',
+                    name: '"compactFooter"',
+                    tip: 'When enabled, Ignite will not write fields metadata when serializing objects (this will increase serialization performance), because internally <b>BinaryMarshaller</b> already distribute metadata inside cluster'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterBinary')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/cache-key-cfg.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/cache-key-cfg.pug
new file mode 100644
index 0000000..04da3db
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/cache-key-cfg.pug
@@ -0,0 +1,66 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'cacheKeyCfg'
+-var model = '$ctrl.clonedCluster.cacheKeyConfiguration'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Cache key configuration
+    panel-description
+        | Cache key configuration allows to collocate objects in a partitioned cache based on field in cache key without explicit usage of annotations on user classes.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6
+            .ignite-form-field
+                +form-field__label({ label: 'Cache key configuration:', name: '"cacheKeyConfiguration"' })
+
+                list-editable.pc-list-editable-with-form-grid(ng-model=model name='cacheKeyConfiguration')
+                    list-editable-item-edit.pc-form-grid-row
+                        - form = '$parent.form'
+                        .pc-form-grid-col-60
+                            +form-field__java-class({
+                                label: 'Type name:',
+                                model: '$item.typeName',
+                                name: '"cacheKeyTypeName"',
+                                required: 'true',
+                                tip: 'Type name'
+                            })(
+                                ignite-form-field-input-autofocus='true'
+                                ignite-unique=model
+                                ignite-unique-property='typeName'
+                            )
+                                +form-field__error({ error: 'igniteUnique', message: 'Type name should be unique.' })
+                        .pc-form-grid-col-60
+                            +form-field__text({
+                                label: 'Affinity key field name:',
+                                model: '$item.affinityKeyFieldName',
+                                name: '"affinityKeyFieldName"',
+                                placeholder: 'Enter field name',
+                                tip: 'Affinity key field name',
+                                required: true
+                            })
+
+                    list-editable-no-items
+                        list-editable-add-item-button(
+                            add-item=`(${model} = ${model} || []).push({})`
+                            label-single='configuration'
+                            label-multiple='configurations'
+                        )
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterCacheKeyConfiguration')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/checkpoint.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/checkpoint.pug
new file mode 100644
index 0000000..1fbec3a
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/checkpoint.pug
@@ -0,0 +1,109 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'checkpoint'
+-var model = '$ctrl.clonedCluster.checkpointSpi'
+-var CustomCheckpoint = '$checkpointSPI.kind === "Custom"'
+-var CacheCheckpoint = '$checkpointSPI.kind === "Cache"'
+
+panel-collapsible(ng-form=form)
+    panel-title Checkpointing
+    panel-description
+        | Checkpointing provides an ability to save an intermediate job state.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/checkpointing" target="_blank") More info]
+    panel-content.pca-form-row
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                .ignite-form-field
+                    +form-field__label({ label: 'Checkpoint SPI configurations:', name: '"checkpointSPIConfigurations"' })
+
+                    list-editable.pc-list-editable-with-form-grid(ng-model=model name='checkpointSPIConfigurations')
+                        list-editable-item-edit(item-name='$checkpointSPI').pc-form-grid-row
+                            .pc-form-grid-col-60
+                                +form-field__dropdown({
+                                    label: 'Checkpoint SPI:',
+                                    model: '$checkpointSPI.kind',
+                                    name: '"checkpointKind"',
+                                    required: 'true',
+                                    placeholder: 'Choose checkpoint configuration variant',
+                                    options: '[\
+                                                {value: "FS", label: "File System"},\
+                                                {value: "Cache", label: "Cache"},\
+                                                {value: "S3", label: "Amazon S3"},\
+                                                {value: "JDBC", label: "Database"},\
+                                                {value: "Custom", label: "Custom"}\
+                                               ]',
+                                    tip: 'Provides an ability to save an intermediate job state\
+                                          <ul>\
+                                            <li>File System - Uses a shared file system to store checkpoints</li>\
+                                            <li>Cache - Uses a cache to store checkpoints</li>\
+                                            <li>Amazon S3 - Uses Amazon S3 to store checkpoints</li>\
+                                            <li>Database - Uses a database to store checkpoints</li>\
+                                            <li>Custom - Custom checkpoint SPI implementation</li>\
+                                          </ul>'
+                                })
+
+                            include ./checkpoint/fs
+
+                            .pc-form-grid-col-60(ng-if-start=CacheCheckpoint)
+                                +form-field__dropdown({
+                                    label: 'Cache:',
+                                    model: '$checkpointSPI.Cache.cache',
+                                    name: '"checkpointCacheCache"',
+                                    required: CacheCheckpoint,
+                                    placeholder: 'Choose cache',
+                                    placeholderEmpty: 'No caches configured for current cluster',
+                                    options: '$ctrl.cachesMenu',
+                                    tip: 'Cache to use for storing checkpoints'
+                                })(
+                                    pc-is-in-collection='$ctrl.clonedCluster.caches'
+                                )
+                                    +form-field__error({ error: 'isInCollection', message: `Cluster doesn't have such a cache` })
+                            .pc-form-grid-col-60(ng-if-end)
+                                +form-field__java-class({
+                                    label: 'Listener:',
+                                    model: '$checkpointSPI.Cache.checkpointListener',
+                                    name: '"checkpointCacheListener"',
+                                    tip: 'Checkpoint listener implementation class name',
+                                    validationActive: CacheCheckpoint
+                                })
+
+                            include ./checkpoint/s3
+
+                            include ./checkpoint/jdbc
+
+                            .pc-form-grid-col-60(ng-if=CustomCheckpoint)
+                                +form-field__java-class({
+                                    label: 'Class name:',
+                                    model: '$checkpointSPI.Custom.className',
+                                    name: '"checkpointCustomClassName"',
+                                    required: CustomCheckpoint,
+                                    tip: 'Custom CheckpointSpi implementation class',
+                                    validationActive: CustomCheckpoint
+                                })
+
+                        list-editable-no-items
+                            list-editable-add-item-button(
+                                add-item=`$edit($ctrl.Clusters.addCheckpointSPI($ctrl.clonedCluster))`
+                                label-single='checkpoint SPI configuration'
+                                label-multiple='checkpoint SPI configurations'
+                            )
+
+        .pca-form-column-6
+            +preview-xml-java('$ctrl.clonedCluster', 'clusterCheckpoint', '$ctrl.caches')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/checkpoint/fs.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/checkpoint/fs.pug
new file mode 100644
index 0000000..0cda6fa
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/checkpoint/fs.pug
@@ -0,0 +1,42 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+.pc-form-grid-col-60(ng-if-start='$checkpointSPI.kind === "FS"')
+    .ignite-form-field
+        +list-text-field({
+            items: `$checkpointSPI.FS.directoryPaths`,
+            lbl: 'Directory path',
+            name: 'directoryPath',
+            itemName: 'path',
+            itemsName: 'paths'
+        })(
+            list-editable-cols=`::[{
+                name: 'Paths:',
+                tip: 'Paths to a shared directory where checkpoints will be stored'
+            }]`
+        )
+            +form-field__error({ error: 'igniteUnique', message: 'Such path already exists!' })
+
+.pc-form-grid-col-60(ng-if-end)
+    +form-field__java-class({
+        label: 'Listener:',
+        model: '$checkpointSPI.FS.checkpointListener',
+        name: '"checkpointFsListener"',
+        tip: 'Checkpoint listener implementation class name',
+        validationActive: '$checkpointSPI.kind === "FS"'
+    })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/checkpoint/jdbc.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/checkpoint/jdbc.pug
new file mode 100644
index 0000000..945f54d
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/checkpoint/jdbc.pug
@@ -0,0 +1,126 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var jdbcCheckpoint = '$checkpointSPI.kind === "JDBC"'
+
+.pc-form-grid-col-30(ng-if-start='$checkpointSPI.kind === "JDBC"')
+    +form-field__text({
+        label: 'Data source bean name:',
+        model: '$checkpointSPI.JDBC.dataSourceBean',
+        name: '"checkpointJdbcDataSourceBean"',
+        required: jdbcCheckpoint,
+        placeholder: 'Input bean name',
+        tip: 'Name of the data source bean in Spring context'
+    })
+.pc-form-grid-col-30
+    +form-field__dialect({
+        label: 'Dialect:',
+        model: '$checkpointSPI.JDBC.dialect',
+        name: '"checkpointJdbcDialect"',
+        required: jdbcCheckpoint,
+        tip: 'Dialect of SQL implemented by a particular RDBMS:',
+        genericDialectName: 'Generic JDBC dialect',
+        placeholder: 'Choose JDBC dialect'
+    })
+.pc-form-grid-col-60(ng-if='$ctrl.Clusters.requiresProprietaryDrivers($checkpointSPI.JDBC)')
+    a.link-success(ng-href='{{ $ctrl.Clusters.jdbcDriverURL($checkpointSPI.JDBC) }}' target='_blank')
+        | Download JDBC drivers?
+.pc-form-grid-col-60
+    +form-field__java-class({
+        label: 'Listener:',
+        model: '$checkpointSPI.JDBC.checkpointListener',
+        name: '"checkpointJdbcListener"',
+        tip: 'Checkpoint listener implementation class name',
+        validationActive: jdbcCheckpoint
+    })
+.pc-form-grid-col-60
+    +form-field__text({
+        label: 'User:',
+        model: '$checkpointSPI.JDBC.user',
+        name: '"checkpointJdbcUser"',
+        placeholder: 'Input user name',
+        tip: 'Checkpoint jdbc user name'
+    })
+.pc-form-grid-col-30
+    +form-field__text({
+        label: 'Table name:',
+        model: '$checkpointSPI.JDBC.checkpointTableName',
+        name: '"checkpointJdbcCheckpointTableName"',
+        placeholder: 'CHECKPOINTS',
+        tip: 'Checkpoint table name'
+    })
+.pc-form-grid-col-30
+    +form-field__number({
+        label: 'Number of retries:',
+        model: '$checkpointSPI.JDBC.numberOfRetries',
+        name: '"checkpointJdbcNumberOfRetries"',
+        placeholder: '2',
+        min: '0',
+        tip: 'Number of retries in case of DB failure'
+    })
+.pc-form-grid-col-30
+    +form-field__text({
+        label: 'Key field name:',
+        model: '$checkpointSPI.JDBC.keyFieldName',
+        name: '"checkpointJdbcKeyFieldName"',
+        placeholder: 'NAME',
+        tip: 'Checkpoint key field name'
+    })
+.pc-form-grid-col-30
+    +form-field__dropdown({
+        label: 'Key field type:',
+        model: '$checkpointSPI.JDBC.keyFieldType',
+        name: '"checkpointJdbcKeyFieldType"',
+        placeholder: 'VARCHAR',
+        options: '::$ctrl.supportedJdbcTypes',
+        tip: 'Checkpoint key field type'
+    })
+.pc-form-grid-col-30
+    +form-field__text({
+        label: 'Value field name:',
+        model: '$checkpointSPI.JDBC.valueFieldName',
+        name: '"checkpointJdbcValueFieldName"',
+        placeholder: 'VALUE',
+        tip: 'Checkpoint value field name'
+    })
+.pc-form-grid-col-30
+    +form-field__dropdown({
+        label: 'Value field type:',
+        model: '$checkpointSPI.JDBC.valueFieldType',
+        name: '"checkpointJdbcValueFieldType"',
+        placeholder: 'BLOB',
+        options: '::$ctrl.supportedJdbcTypes',
+        tip: 'Checkpoint value field type'
+    })
+.pc-form-grid-col-30
+    +form-field__text({
+        label:'Expire date field name:',
+        model: '$checkpointSPI.JDBC.expireDateFieldName',
+        name: '"checkpointJdbcExpireDateFieldName"',
+        placeholder: 'EXPIRE_DATE',
+        tip: 'Checkpoint expire date field name'
+    })
+.pc-form-grid-col-30(ng-if-end)
+    +form-field__dropdown({
+        label: 'Expire date field type:',
+        model: '$checkpointSPI.JDBC.expireDateFieldType',
+        name: '"checkpointJdbcExpireDateFieldType"',
+        placeholder: 'DATETIME',
+        options: '::$ctrl.supportedJdbcTypes',
+        tip: 'Checkpoint expire date field type'
+    })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/checkpoint/s3.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/checkpoint/s3.pug
new file mode 100644
index 0000000..1f6eef2
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/checkpoint/s3.pug
@@ -0,0 +1,443 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var credentialsModel = '$checkpointSPI.S3.awsCredentials'
+-var clientCfgModel = '$checkpointSPI.S3.clientConfiguration'
+-var checkpointS3 = '$checkpointSPI.kind === "S3"'
+-var checkpointS3Path = checkpointS3 + ' && $checkpointSPI.S3.awsCredentials.kind === "Properties"'
+-var checkpointS3Custom = checkpointS3 + ' && $checkpointSPI.S3.awsCredentials.kind === "Custom"'
+
+-var clientRetryModel = clientCfgModel + '.retryPolicy'
+-var checkpointS3DefaultMaxRetry = checkpointS3 + ' && ' + clientRetryModel + '.kind === "DefaultMaxRetries"'
+-var checkpointS3DynamoDbMaxRetry = checkpointS3 + ' && ' + clientRetryModel + '.kind === "DynamoDBMaxRetries"'
+-var checkpointS3CustomRetry = checkpointS3 + ' && ' + clientRetryModel + '.kind === "Custom"'
+
+.pc-form-grid-col-60(ng-if-start='$checkpointSPI.kind === "S3"')
+    +form-field__dropdown({
+        label: 'AWS credentials:',
+        model: '$checkpointSPI.S3.awsCredentials.kind',
+        name: '"checkpointS3AwsCredentials"',
+        required: checkpointS3,
+        placeholder: 'Custom',
+        options: '[\
+                {value: "Basic", label: "Basic"},\
+                {value: "Properties", label: "Properties"},\
+                {value: "Anonymous", label: "Anonymous"},\
+                {value: "BasicSession", label: "Basic with session"},\
+                {value: "Custom", label: "Custom"}\
+            ]',
+        tip: 'AWS credentials\
+            <ul>\
+                <li>Basic - Allows callers to pass in the AWS access key and secret access in the constructor</li>\
+                <li>Properties - Reads in AWS access keys from a properties file</li>\
+                <li>Anonymous - Allows use of "anonymous" credentials</li>\
+                <li>Database - Session credentials with keys and session token</li>\
+                <li>Custom - Custom AWS credentials provider</li>\
+            </ul>'
+    })
+
+.pc-form-group.pc-form-grid-row(ng-if=checkpointS3Path)
+    .pc-form-grid-col-60
+        +form-field__text({
+            label: 'Path:',
+            model: `${credentialsModel}.Properties.path`,
+            name: '"checkpointS3PropertiesPath"',
+            required: checkpointS3Path,
+            placeholder: 'Input properties file path',
+            tip: 'The file from which to read the AWS credentials properties'
+        })
+.pc-form-group.pc-form-grid-row(ng-if=checkpointS3Custom)
+    .pc-form-grid-col-60
+        +form-field__java-class({
+            label: 'Class name:',
+            model: credentialsModel + '.Custom.className',
+            name: '"checkpointS3CustomClassName"',
+            required: checkpointS3Custom,
+            tip: 'Custom AWS credentials provider implementation class',
+            validationActive:checkpointS3Custom
+        })
+.pc-form-grid-col-60
+    label Note, AWS credentials will be generated as stub
+.pc-form-grid-col-60
+    +form-field__text({
+        label: 'Bucket name suffix:',
+        model: '$checkpointSPI.S3.bucketNameSuffix',
+        name: '"checkpointS3BucketNameSuffix"',
+        placeholder: 'default-bucket'
+    })
+.pc-form-grid-col-60(ng-if-start=`$ctrl.available("2.4.0")`)
+    +form-field__text({
+        label: 'Bucket endpoint:',
+        model: `$checkpointSPI.S3.bucketEndpoint`,
+        name: '"checkpointS3BucketEndpoint"',
+        placeholder: 'Input bucket endpoint',
+        tip: 'Bucket endpoint for IP finder<br/> \
+            For information about possible endpoint names visit <a href="http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region">docs.aws.amazon.com</a>'
+    })
+.pc-form-grid-col-60(ng-if-end)
+    +form-field__text({
+        label: 'SSE algorithm:',
+        model: `$checkpointSPI.S3.SSEAlgorithm`,
+        name: '"checkpointS3SseAlgorithm"',
+        placeholder: 'Input SSE algorithm',
+        tip: 'Server-side encryption algorithm for Amazon S3-managed encryption keys<br/> \
+              For information about possible S3-managed encryption keys visit <a href="http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html">docs.aws.amazon.com</a>'
+    })
+.pc-form-grid-col-60
+    +form-field__java-class({
+        label: 'Listener:',
+        model: '$checkpointSPI.S3.checkpointListener',
+        name: '"checkpointS3Listener"',
+        tip: 'Checkpoint listener implementation class name',
+        validationActive: checkpointS3
+    })
+.pc-form-grid-col-60.pc-form-group__text-title
+    span Client configuration
+.pc-form-group.pc-form-grid-row(ng-if-end)
+    .pc-form-grid-col-30
+        +form-field__dropdown({
+            label: 'Protocol:',
+            model: clientCfgModel + '.protocol',
+            name: '"checkpointS3Protocol"',
+            placeholder: 'HTTPS',
+            options: '[\
+                            {value: "HTTP", label: "HTTP"},\
+                            {value: "HTTPS", label: "HTTPS"}\
+                        ]',
+            tip: 'Provides an ability to save an intermediate job state\
+                    <ul>\
+                        <li>HTTP - Using the HTTP protocol is less secure than HTTPS, but can slightly reduce\
+                            the system resources used when communicating with AWS</li>\
+                        <li>HTTPS - Using the HTTPS protocol is more secure than using the HTTP protocol, but\
+                            may use slightly more system resources. AWS recommends using HTTPS for maximize security</li>\
+                    </ul>'
+        })
+    .pc-form-grid-col-30
+        +form-field__number({
+            label:'Maximum connections:',
+            model:clientCfgModel + '.maxConnections',
+            name: '"checkpointS3MaxConnections"',
+            placeholder: '50',
+            min: '1',
+            tip: 'Maximum number of allowed open HTTP connections'
+        })
+    .pc-form-grid-col-60
+        +form-field__text({
+            label: 'User agent prefix:',
+            model: `${clientCfgModel}.userAgentPrefix`,
+            name: '"checkpointS3UserAgentPrefix"',
+            placeholder: 'System specific header',
+            tip: 'HTTP user agent prefix to send with all requests'
+        })
+    .pc-form-grid-col-60
+        +form-field__text({
+            label: 'User agent suffix:',
+            model: `${clientCfgModel}.userAgentSuffix`,
+            name: '"checkpointS3UserAgentSuffix"',
+            placeholder: 'System specific header',
+            tip: 'HTTP user agent suffix to send with all requests'
+        })
+    .pc-form-grid-col-60
+        +form-field__ip-address({
+            label: 'Local address:',
+            model: clientCfgModel + '.localAddress',
+            name: '"checkpointS3LocalAddress"',
+            enabled: 'true',
+            placeholder: 'Not specified',
+            tip: 'Optionally specifies the local address to bind to'
+        })
+    .pc-form-grid-col-40
+        +form-field__text({
+            label: 'Proxy host:',
+            model: `${clientCfgModel}.proxyHost`,
+            name: '"checkpointS3ProxyHost"',
+            placeholder: 'Not specified',
+            tip: 'Optional proxy host the client will connect through'
+        })
+    .pc-form-grid-col-20
+        +form-field__number({
+            label: 'Proxy port:',
+            model: clientCfgModel + '.proxyPort',
+            name: '"checkpointS3ProxyPort"',
+            placeholder: 'Not specified',
+            min: '0',
+            tip: 'Optional proxy port the client will connect through'
+        })
+    .pc-form-grid-col-30
+        +form-field__text({
+            label: 'Proxy user:',
+            model: clientCfgModel + '.proxyUsername',
+            name: '"checkpointS3ProxyUsername"',
+            placeholder: 'Not specified',
+            tip: 'Optional proxy user name to use if connecting through a proxy'
+        })
+    .pc-form-grid-col-30
+        +form-field__text({
+            label: 'Proxy domain:',
+            model: `${clientCfgModel}.proxyDomain`,
+            name: '"checkpointS3ProxyDomain"',
+            placeholder: 'Not specified',
+            tip: 'Optional Windows domain name for configuring an NTLM proxy'
+        })
+    .pc-form-grid-col-60
+        +form-field__text({
+            label: 'Proxy workstation:',
+            model: `${clientCfgModel}.proxyWorkstation`,
+            name: '"checkpointS3ProxyWorkstation"',
+            placeholder: 'Not specified',
+            tip: 'Optional Windows workstation name for configuring NTLM proxy support'
+        })
+    .pc-form-grid-col-60
+        +form-field__text({
+            label: 'Non proxy hosts:',
+            model: `${clientCfgModel}.nonProxyHosts`,
+            name: '"checkpointS3NonProxyHosts"',
+            placeholder: 'Not specified',
+            tip: 'Optional hosts the client will access without going through the proxy'
+        })
+    .pc-form-grid-col-60
+        +form-field__dropdown({
+            label: 'Retry policy:',
+            model: `${clientRetryModel}.kind`,
+            name: '"checkpointS3RetryPolicy"',
+            placeholder: 'Default',
+            options: '[\
+                                                        {value: "Default", label: "Default SDK retry policy"},\
+                                                        {value: "DefaultMaxRetries", label: "Default with the specified max retry count"},\
+                                                        {value: "DynamoDB", label: "Default for DynamoDB client"},\
+                                                        {value: "DynamoDBMaxRetries", label: "DynamoDB with the specified max retry count"},\
+                                                        {value: "Custom", label: "Custom configured"}\
+                                                    ]',
+            tip: 'Provides an ability to save an intermediate job state\
+                    <ul>\
+                        <li>SDK default retry policy - This policy will honor the maxErrorRetry set in ClientConfiguration</li>\
+                        <li>Default with the specified max retry count - Default SDK retry policy with the specified max retry count</li>\
+                        <li>Default for DynamoDB client - This policy will honor the maxErrorRetry set in ClientConfiguration</li>\
+                        <li>DynamoDB with the specified max retry count - This policy will honor the maxErrorRetry set in ClientConfiguration with the specified max retry count</li>\
+                        <li>Custom configured - Custom configured SDK retry policy</li>\
+                    </ul>'
+        })
+    .pc-form-group.pc-form-grid-row(ng-if=checkpointS3DefaultMaxRetry)
+        .pc-form-grid-col-60
+            +form-field__number({
+                label: 'Maximum retry attempts:',
+                model: clientRetryModel + '.DefaultMaxRetries.maxErrorRetry',
+                name: '"checkpointS3DefaultMaxErrorRetry"',
+                required: checkpointS3DefaultMaxRetry,
+                placeholder: '-1',
+                min: '1',
+                tip: 'Maximum number of retry attempts for failed requests'
+            })
+    .pc-form-group.pc-form-grid-row(ng-if=checkpointS3DynamoDbMaxRetry)
+        .pc-form-grid-col-60
+            +form-field__number({
+                label: 'Maximum retry attempts:',
+                model: clientRetryModel + '.DynamoDBMaxRetries.maxErrorRetry',
+                name: '"checkpointS3DynamoDBMaxErrorRetry"',
+                required: checkpointS3DynamoDbMaxRetry,
+                placeholder: '-1',
+                min: '1',
+                tip: 'Maximum number of retry attempts for failed requests'
+            })
+    .pc-form-group.pc-form-grid-row(ng-if=checkpointS3CustomRetry)
+        .pc-form-grid-col-60
+            +form-field__java-class({
+                label: 'Retry condition:',
+                model: clientRetryModel + '.Custom.retryCondition',
+                name: '"checkpointS3CustomRetryPolicy"',
+                required: checkpointS3CustomRetry,
+                tip: 'Retry condition on whether a specific request and exception should be retried',
+                validationActive: checkpointS3CustomRetry
+            })
+        .pc-form-grid-col-60
+            +form-field__java-class({
+                label: 'Backoff strategy:',
+                model: clientRetryModel + '.Custom.backoffStrategy',
+                name: '"checkpointS3CustomBackoffStrategy"',
+                required: checkpointS3CustomRetry,
+                tip: 'Back-off strategy for controlling how long the next retry should wait',
+                validationActive: checkpointS3CustomRetry
+            })
+        .pc-form-grid-col-60
+            +form-field__number({
+                label: 'Maximum retry attempts:',
+                model: clientRetryModel + '.Custom.maxErrorRetry',
+                name: '"checkpointS3CustomMaxErrorRetry"',
+                required: checkpointS3CustomRetry,
+                placeholder: '-1',
+                min: '1',
+                tip: 'Maximum number of retry attempts for failed requests'
+            })
+        .pc-form-grid-col-60
+            +form-field__checkbox({
+                label: 'Honor the max error retry set',
+                model: clientRetryModel + '.Custom.honorMaxErrorRetryInClientConfig',
+                name: '"checkpointS3CustomHonorMaxErrorRetryInClientConfig"',
+                tip: 'Whether this retry policy should honor the max error retry set by ClientConfiguration#setMaxErrorRetry(int)'
+            })
+    .pc-form-grid-col-60
+        +form-field__number({
+            label: 'Maximum retry attempts:',
+            model: `${clientCfgModel}.maxErrorRetry`,
+            name: '"checkpointS3MaxErrorRetry"',
+            placeholder: '-1',
+            min: '0',
+            tip: 'Maximum number of retry attempts for failed retryable requests<br/>\
+                  If -1 the configured RetryPolicy will be used to control the retry count'
+        })
+    .pc-form-grid-col-30
+        +form-field__number({
+            label: 'Socket timeout:',
+            model: `${clientCfgModel}.socketTimeout`,
+            name: '"checkpointS3SocketTimeout"',
+            placeholder: '50000',
+            min: '0',
+            tip: 'Amount of time in milliseconds to wait for data to be transfered over an established, open connection before the connection times out and is closed<br/>\
+                  A value of <b>0</b> means infinity'
+        })
+    .pc-form-grid-col-30
+        +form-field__number({
+            label: 'Connection timeout:',
+            model: `${clientCfgModel}.connectionTimeout`,
+            name: '"checkpointS3ConnectionTimeout"',
+            placeholder: '50000',
+            min: '0',
+            tip: 'Amount of time in milliseconds to wait when initially establishing a connection before giving up and timing out<br/>\
+                  A value of <b>0</b> means infinity'
+        })
+    .pc-form-grid-col-30
+        +form-field__number({
+            label: 'Request timeout:',
+            model: `${clientCfgModel}.requestTimeout`,
+            name: '"checkpointS3RequestTimeout"',
+            placeholder: '0',
+            min: '-1',
+            tip: 'Amount of time in milliseconds to wait for the request to complete before giving up and timing out<br/>\
+                  A non - positive value means infinity'
+        })
+    .pc-form-grid-col-30
+        +form-field__number({
+            label: 'Idle timeout:',
+            model: `${clientCfgModel}.connectionMaxIdleMillis`,
+            name: '"checkpointS3ConnectionMaxIdleMillis"',
+            placeholder: '60000',
+            min: '0',
+            tip: 'Maximum amount of time that an idle connection may sit in the connection pool and still be eligible for reuse'
+        })
+    .pc-form-grid-col-30
+        +form-field__text({
+            label: 'Signature algorithm:',
+            model: `${clientCfgModel}.signerOverride`,
+            name: '"checkpointS3SignerOverride"',
+            placeholder: 'Not specified',
+            tip: 'Name of the signature algorithm to use for signing requests made by this client'
+        })
+    .pc-form-grid-col-30
+        +form-field__number({
+            label: 'Connection TTL:',
+            model: `${clientCfgModel}.connectionTTL`,
+            name: '"checkpointS3ConnectionTTL"',
+            placeholder: '-1',
+            min: '-1',
+            tip: 'Expiration time in milliseconds for a connection in the connection pool<br/>\
+                  By default, it is set to <b>-1</b>, i.e. connections do not expire'
+        })
+    .pc-form-grid-col-60
+        +form-field__java-class({
+            label: 'DNS resolver:',
+            model: clientCfgModel + '.dnsResolver',
+            name: '"checkpointS3DnsResolver"',
+            tip: 'DNS Resolver that should be used to for resolving AWS IP addresses',
+            validationActive: checkpointS3
+        })
+    .pc-form-grid-col-60
+        +form-field__number({
+            label: 'Response metadata cache size:',
+            model: `${clientCfgModel}.responseMetadataCacheSize`,
+            name: '"checkpointS3ResponseMetadataCacheSize"',
+            placeholder: '50',
+            min: '0',
+            tip: 'Response metadata cache size'
+        })
+    .pc-form-grid-col-60
+        +form-field__java-class({
+            label: 'SecureRandom class name:',
+            model: clientCfgModel + '.secureRandom',
+            name: '"checkpointS3SecureRandom"',
+            tip: 'SecureRandom to be used by the SDK class name',
+            validationActive: checkpointS3
+        })
+    .pc-form-grid-col-60
+        +form-field__number({
+            label: 'Client execution timeout:',
+            model: `${clientCfgModel}.clientExecutionTimeout`,
+            name: '"checkpointS3ClientExecutionTimeout"',
+            placeholder: '0',
+            min: '0',
+            tip: 'Amount of time in milliseconds to allow the client to complete the execution of an API call<br/>\
+                  <b>0</b> value disables that feature'
+        })
+    .pc-form-grid-col-60
+        +form-field__checkbox({
+            label: 'Cache response metadata',
+            model: clientCfgModel + '.cacheResponseMetadata',
+            name: '"checkpointS3CacheResponseMetadata"',
+            tip: 'Cache response metadata'
+        })
+    .pc-form-grid-col-60
+        +form-field__checkbox({
+            label: 'Use expect continue',
+            model: clientCfgModel + '.useExpectContinue',
+            name: '"checkpointS3UseExpectContinue"',
+            tip: 'Optional override to enable/disable support for HTTP/1.1 handshake utilizing EXPECT: 100-Continue'
+        })
+    .pc-form-grid-col-60
+        +form-field__checkbox({
+            label: 'Use throttle retries',
+            model: clientCfgModel + '.useThrottleRetries',
+            name: '"checkpointS3UseThrottleRetries"',
+            tip: 'Retry throttling will be used'
+        })
+    .pc-form-grid-col-60
+        +form-field__checkbox({
+            label: 'Use reaper',
+            model: clientCfgModel + '.useReaper',
+            name: '"checkpointS3UseReaper"',
+            tip: 'Checks if the IdleConnectionReaper is to be started'
+        })
+    .pc-form-grid-col-60
+        +form-field__checkbox({
+            label: 'Use GZIP',
+            model: clientCfgModel + '.useGzip',
+            name: '"checkpointS3UseGzip"',
+            tip: 'Checks if gzip compression is used'
+        })
+    .pc-form-grid-col-60
+        +form-field__checkbox({
+            label: 'Preemptively basic authentication',
+            model: clientCfgModel + '.preemptiveBasicProxyAuth',
+            name: '"checkpointS3PreemptiveBasicProxyAuth"',
+            tip: 'Attempt to authenticate preemptively against proxy servers using basic authentication'
+        })
+    .pc-form-grid-col-60
+        +form-field__checkbox({
+            label: 'TCP KeepAlive',
+            model: clientCfgModel + '.useTcpKeepAlive',
+            name: '"checkpointS3UseTcpKeepAlive"',
+            tip: 'TCP KeepAlive support is enabled'
+        })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/client-connector.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/client-connector.pug
new file mode 100644
index 0000000..d427665
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/client-connector.pug
@@ -0,0 +1,184 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'clientConnector'
+-var model = '$ctrl.clonedCluster'
+-var connectionModel = `${model}.clientConnectorConfiguration`
+-var connectionEnabled = `${connectionModel}.enabled`
+-var sslEnabled = `${connectionEnabled} && ${connectionModel}.sslEnabled`
+-var sslFactoryEnabled = `${sslEnabled} && !${connectionModel}.useIgniteSslContextFactory`
+
+panel-collapsible(ng-show='$ctrl.available("2.3.0")' ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Client connector configuration
+    panel-content.pca-form-row(ng-if=`$ctrl.available("2.3.0") && ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enabled',
+                    model: connectionEnabled,
+                    name: '"ClientConnectorEnabled"',
+                    tip: 'Flag indicating whether to configure client connector configuration'
+                })
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'Host:',
+                    model: `${connectionModel}.host`,
+                    name: '"ClientConnectorHost"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: 'localhost'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Port:',
+                    model: `${connectionModel}.port`,
+                    name: '"ClientConnectorPort"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: '10800',
+                    min: '1025'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Port range:',
+                    model: `${connectionModel}.portRange`,
+                    name: '"ClientConnectorPortRange"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: '100',
+                    min: '0'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Socket send buffer size:',
+                    model: `${connectionModel}.socketSendBufferSize`,
+                    name: '"ClientConnectorSocketSendBufferSize"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Socket send buffer size<br/>\
+                          When set to <b>0</b>, operation system default will be used'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Socket receive buffer size:',
+                    model: `${connectionModel}.socketReceiveBufferSize`,
+                    name: '"ClientConnectorSocketReceiveBufferSize"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Socket receive buffer size<br/>\
+                          When set to <b>0</b>, operation system default will be used'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Max connection cursors:',
+                    model: `${connectionModel}.maxOpenCursorsPerConnection`,
+                    name: '"ClientConnectorMaxOpenCursorsPerConnection"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: '128',
+                    min: '0',
+                    tip: 'Max number of opened cursors per connection'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Pool size:',
+                    model: `${connectionModel}.threadPoolSize`,
+                    name: '"ClientConnectorThreadPoolSize"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: 'max(8, availableProcessors)',
+                    min: '1',
+                    tip: 'Size of thread pool that is in charge of processing SQL requests'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'TCP_NODELAY option',
+                    model: `${connectionModel}.tcpNoDelay`,
+                    name: '"ClientConnectorTcpNoDelay"',
+                    disabled: `!${connectionEnabled}`
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.4.0")')
+                +form-field__number({
+                    label: 'Idle timeout:',
+                    model: `${connectionModel}.idleTimeout`,
+                    name: '"ClientConnectorIdleTimeout"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: '0',
+                    min: '-1',
+                    tip: 'Idle timeout for client connections<br/>\
+                         Zero or negative means no timeout'
+                })
+            .pc-form-grid-col-60(ng-if-start='$ctrl.available("2.5.0")')
+                +form-field__checkbox({
+                    label: 'Enable SSL',
+                    model: `${connectionModel}.sslEnabled`,
+                    name: '"ClientConnectorSslEnabled"',
+                    disabled: `!${connectionEnabled}`,
+                    tip: 'Enable secure socket layer on client connector'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enable SSL client auth',
+                    model: `${connectionModel}.sslClientAuth`,
+                    name: '"ClientConnectorSslClientAuth"',
+                    disabled: `!(${sslEnabled})`,
+                    tip: 'Flag indicating whether or not SSL client authentication is required'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Use Ignite SSL',
+                    model: `${connectionModel}.useIgniteSslContextFactory`,
+                    name: '"ClientConnectorUseIgniteSslContextFactory"',
+                    disabled: `!(${sslEnabled})`,
+                    tip: 'Use SSL factory Ignite configuration'
+                })
+            .pc-form-grid-col-60(ng-if-end)
+                +form-field__java-class({
+                    label:'SSL factory:',
+                    model: `${connectionModel}.sslContextFactory`,
+                    name: '"ClientConnectorSslContextFactory"',
+                    disabled: `!(${sslFactoryEnabled})`,
+                    required: sslFactoryEnabled,
+                    tip: 'If SSL factory specified then replication will be performed through secure SSL channel created with this factory<br/>\
+                          If not present <b>isUseIgniteSslContextFactory()</b> flag will be evaluated<br/>\
+                          If set to <b>true</b> and <b>IgniteConfiguration#getSslContextFactory()</b> exists, then Ignite SSL context factory will be used to establish secure connection'
+                })
+            .pc-form-grid-col-60(ng-if-start='$ctrl.available("2.4.0")')
+                +form-field__checkbox({
+                    label: 'JDBC Enabled',
+                    model: `${connectionModel}.jdbcEnabled`,
+                    name: '"ClientConnectorJdbcEnabled"',
+                    disabled: `!${connectionEnabled}`,
+                    tip: 'Access through JDBC is enabled'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'ODBC Enabled',
+                    model: `${connectionModel}.odbcEnabled`,
+                    name: '"ClientConnectorOdbcEnabled"',
+                    disabled: `!${connectionEnabled}`,
+                    tip: 'Access through ODBC is enabled'
+                })
+            .pc-form-grid-col-60(ng-if-end)
+                +form-field__checkbox({
+                    label: 'Thin client enabled',
+                    model: `${connectionModel}.thinClientEnabled`,
+                    name: '"ClientConnectorThinCliEnabled"',
+                    disabled: `!${connectionEnabled}`,
+                    tip: 'Access through thin client is enabled'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterClientConnector')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision.pug
new file mode 100644
index 0000000..c0e02be
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision.pug
@@ -0,0 +1,64 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'collision'
+-var model = '$ctrl.clonedCluster.collision'
+-var modelCollisionKind = model + '.kind';
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Collision configuration
+    panel-description
+        | Configuration Collision SPI allows to regulate how grid jobs get executed when they arrive on a destination node for execution.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/job-scheduling" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label:'CollisionSpi:',
+                    model: modelCollisionKind,
+                    name: '"collisionKind"',
+                    placeholder: 'Choose discovery',
+                    options: '[\
+                        {value: "JobStealing", label: "Job stealing"},\
+                        {value: "FifoQueue", label: "FIFO queue"},\
+                        {value: "PriorityQueue", label: "Priority queue"},\
+                        {value: "Custom", label: "Custom"},\
+                        {value: "Noop", label: "Default"}\
+                    ]',
+                    tip: 'Regulate how grid jobs get executed when they arrive on a destination node for execution\
+                       <ul>\
+                           <li>Job stealing - supports job stealing from over-utilized nodes to under-utilized nodes</li>\
+                           <li>FIFO queue - jobs are ordered as they arrived</li>\
+                           <li>Priority queue - jobs are first ordered by their priority</li>\
+                           <li>Custom - custom CollisionSpi implementation</li>\
+                           <li>Default - jobs are activated immediately on arrival to mapped node</li>\
+                       </ul>'
+                })
+            .pc-form-group(ng-show=`${modelCollisionKind} !== 'Noop'`)
+                .pc-form-grid-row(ng-show=`${modelCollisionKind} === 'JobStealing'`)
+                    include ./collision/job-stealing
+                .pc-form-grid-row(ng-show=`${modelCollisionKind} === 'FifoQueue'`)
+                    include ./collision/fifo-queue
+                .pc-form-grid-row(ng-show=`${modelCollisionKind} === 'PriorityQueue'`)
+                    include ./collision/priority-queue
+                .pc-form-grid-row(ng-show=`${modelCollisionKind} === 'Custom'`)
+                    include ./collision/custom
+        .pca-form-column-6
+            -var model = '$ctrl.clonedCluster.collision'
+            +preview-xml-java(model, 'clusterCollision')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision/custom.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision/custom.pug
new file mode 100644
index 0000000..64bd5e4
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision/custom.pug
@@ -0,0 +1,30 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var model = '$ctrl.clonedCluster.collision.Custom'
+-var required = '$ctrl.clonedCluster.collision.kind === "Custom"'
+
+.pc-form-grid-col-60
+    +form-field__java-class({
+        label: 'Class:',
+        model: `${model}.class`,
+        name: '"collisionCustom"',
+        required: required,
+        tip: 'CollisionSpi implementation class',
+        validationActive: required
+    })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision/fifo-queue.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision/fifo-queue.pug
new file mode 100644
index 0000000..de795b7
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision/fifo-queue.pug
@@ -0,0 +1,38 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var model = '$ctrl.clonedCluster.collision.FifoQueue'
+
+.pc-form-grid-col-30
+    +form-field__number({
+        label: 'Parallel jobs number:',
+        model: `${model}.parallelJobsNumber`,
+        name: '"fifoParallelJobsNumber"',
+        placeholder: 'availableProcessors * 2',
+        min: '1',
+        tip: 'Number of jobs that can be executed in parallel'
+    })
+.pc-form-grid-col-30
+    +form-field__number({
+        label: 'Wait jobs number:',
+        model: `${model}.waitingJobsNumber`,
+        name: '"fifoWaitingJobsNumber"',
+        placeholder: 'Integer.MAX_VALUE',
+        min: '0',
+        tip: 'Maximum number of jobs that are allowed to wait in waiting queue'
+    })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision/job-stealing.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision/job-stealing.pug
new file mode 100644
index 0000000..8722544
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision/job-stealing.pug
@@ -0,0 +1,83 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var model = '$ctrl.clonedCluster.collision.JobStealing'
+-var stealingAttributes = `${model}.stealingAttributes`
+
+.pc-form-grid-col-30
+    +form-field__number({
+        label: 'Active jobs threshold:',
+        model: `${model}.activeJobsThreshold`,
+        name: '"jsActiveJobsThreshold"',
+        placeholder: '95',
+        min: '0',
+        tip: 'Number of jobs that can be executed in parallel'
+    })
+.pc-form-grid-col-30
+    +form-field__number({
+        label: 'Wait jobs threshold:',
+        model: `${model}.waitJobsThreshold`,
+        name: '"jsWaitJobsThreshold"',
+        placeholder: '0',
+        min: '0',
+        tip: 'Job count threshold at which this node will start stealing jobs from other nodes'
+    })
+.pc-form-grid-col-30
+    +form-field__number({
+        label: 'Message expire time:',
+        model: `${model}.messageExpireTime`,
+        name: '"jsMessageExpireTime"',
+        placeholder: '1000',
+        min: '1',
+        tip: 'Message expire time in ms'
+    })
+.pc-form-grid-col-30
+    +form-field__number({
+        label: 'Maximum stealing attempts:',
+        model: `${model}.maximumStealingAttempts`,
+        name: '"jsMaximumStealingAttempts"',
+        placeholder: '5',
+        min: '1',
+        tip: 'Maximum number of attempts to steal job by another node'
+    })
+.pc-form-grid-col-60
+    +form-field__checkbox({
+        label: 'Stealing enabled',
+        model: `${model}.stealingEnabled`,
+        name: '"jsStealingEnabled"',
+        tip: 'Node should attempt to steal jobs from other nodes'
+    })
+.pc-form-grid-col-60
+    +form-field__java-class({
+        label: 'External listener:',
+        model: `${model}.externalCollisionListener`,
+        name: '"jsExternalCollisionListener"',
+        tip: 'Listener to be set for notification of external collision events',
+        validationActive: '$ctrl.clonedCluster.collision.kind === "JobStealing"'
+    })
+.pc-form-grid-col-60
+    .ignite-form-field
+        +form-field__label({ label: 'Stealing attributes:', name: '"stealingAttributes"' })
+            +form-field__tooltip(`Configuration parameter to enable stealing to/from only nodes that have these attributes set`)
+        +list-pair-edit({
+            items: stealingAttributes,
+            keyLbl: 'Attribute name',
+            valLbl: 'Attribute value',
+            itemName: 'stealing attribute',
+            itemsName: 'stealing attributes'
+        })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision/priority-queue.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision/priority-queue.pug
new file mode 100644
index 0000000..c8ae733
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/collision/priority-queue.pug
@@ -0,0 +1,79 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var model = '$ctrl.clonedCluster.collision.PriorityQueue'
+
+.pc-form-grid-col-30
+    +form-field__number({
+        label: 'Parallel jobs number:',
+        model: `${model}.parallelJobsNumber`,
+        name: '"priorityParallelJobsNumber"',
+        placeholder: 'availableProcessors * 2',
+        min: '1',
+        tip: 'Number of jobs that can be executed in parallel'
+    })
+.pc-form-grid-col-30
+    +form-field__number({
+        label: 'Waiting jobs number:',
+        model: `${model}.waitingJobsNumber`,
+        name: '"priorityWaitingJobsNumber"',
+        placeholder: 'Integer.MAX_VALUE',
+        min: '0',
+        tip: 'Maximum number of jobs that are allowed to wait in waiting queue'
+    })
+.pc-form-grid-col-30
+    +form-field__text({
+        label: 'Priority attribute key:',
+        model: `${model}.priorityAttributeKey`,
+        name: '"priorityPriorityAttributeKey"',
+        placeholder: 'grid.task.priority',
+        tip: 'Task priority attribute key'
+    })
+.pc-form-grid-col-30
+    +form-field__text({
+        label: 'Job priority attribute key:',
+        model: `${model}.jobPriorityAttributeKey`,
+        name: '"priorityJobPriorityAttributeKey"',
+        placeholder: 'grid.job.priority',
+        tip: 'Job priority attribute key'
+    })
+.pc-form-grid-col-30
+    +form-field__number({
+        label: 'Default priority:',
+        model: `${model}.defaultPriority`,
+        name: '"priorityDefaultPriority"',
+        placeholder: '0',
+        min: '0',
+        tip: 'Default priority to use if a job does not have priority attribute set'
+    })
+.pc-form-grid-col-30
+    +form-field__number({
+        label: 'Starvation increment:',
+        model: `${model}.starvationIncrement`,
+        name: '"priorityStarvationIncrement"',
+        placeholder: '1',
+        min: '0',
+        tip: 'Value to increment job priority by every time a lower priority job gets behind a higher priority job'
+    })
+.pc-form-grid-col-60
+    +form-field__checkbox({
+        label: 'Starvation prevention enabled',
+        model: `${model}.starvationPreventionEnabled`,
+        name: '"priorityStarvationPreventionEnabled"',
+        tip: 'Job starvation prevention is enabled'
+    })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/communication.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/communication.pug
new file mode 100644
index 0000000..41ef206
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/communication.pug
@@ -0,0 +1,318 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'communication'
+-var model = '$ctrl.clonedCluster'
+-var communication = model + '.communication'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Communication
+    panel-description
+        | Configuration of communication with other nodes by TCP/IP.
+        | Provide basic plumbing to send and receive grid messages and is utilized for all distributed grid operations.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/network-config" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Timeout:',
+                    model: `${model}.networkTimeout`,
+                    name: '"commNetworkTimeout"',
+                    placeholder: '5000',
+                    min: '1',
+                    tip: 'Maximum timeout in milliseconds for network requests'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Send retry delay:',
+                    model: `${model}.networkSendRetryDelay`,
+                    name: '"networkSendRetryDelay"',
+                    placeholder: '1000',
+                    min: '1',
+                    tip: 'Interval in milliseconds between message send retries'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Send retry count:',
+                    model: `${model}.networkSendRetryCount`,
+                    name: '"networkSendRetryCount"',
+                    placeholder: '3',
+                    min: '1',
+                    tip: 'Message send retries count'
+                })
+            .pc-form-grid-col-30(ng-if='$ctrl.available("2.8.0")')
+                +form-field__number({
+                    label: 'Compression level:',
+                    model: `${model}.networkCompressionLevel`,
+                    name: '"networkCompressionLevel"',
+                    placeholder: '1',
+                    min: '0',
+                    max: '9',
+                    tip: 'Compression level of internal network messages'
+                })
+            .pc-form-grid-col-30(ng-if='$ctrl.available(["1.0.0", "2.3.0"])')
+                +form-field__number({
+                    label: 'Discovery startup delay:',
+                    model: `${model}.discoveryStartupDelay`,
+                    name: '"discoveryStartupDelay"',
+                    placeholder: '60000',
+                    min: '1',
+                    tip: 'This value is used to expire messages from waiting list whenever node discovery discrepancies happen'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.5.0")')
+                +form-field__java-class({
+                    label: 'Failure resolver:',
+                    model: `${model}.communicationFailureResolver`,
+                    name: '"communicationFailureResolver"',
+                    tip: 'Communication failure resovler'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Communication listener:',
+                    model: `${communication}.listener`,
+                    name: '"comListener"',
+                    tip: 'Listener of communication events'
+                })
+            .pc-form-grid-col-30
+                +form-field__ip-address({
+                    label: 'Local IP address:',
+                    model: `${communication}.localAddress`,
+                    name: '"comLocalAddress"',
+                    enabled: 'true',
+                    placeholder: '0.0.0.0',
+                    tip: 'Local host address for socket binding<br/>\
+                         If not specified use all available addres on local host'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Local port:',
+                    model: `${communication}.localPort`,
+                    name: '"comLocalPort"',
+                    placeholder: '47100',
+                    min: '1024',
+                    max: '65535',
+                    tip: 'Local port for socket binding'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Local port range:',
+                    model: `${communication}.localPortRange`,
+                    name: '"comLocalPortRange"',
+                    placeholder: '100',
+                    min: '1',
+                    tip: 'Local port range for local host ports'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Shared memory port:',
+                    model: `${communication}.sharedMemoryPort`,
+                    name: '"sharedMemoryPort"',
+                    placeholder: '{{ ::$ctrl.Clusters.sharedMemoryPort.default }}',
+                    min: '{{ ::$ctrl.Clusters.sharedMemoryPort.min }}',
+                    max: '{{ ::$ctrl.Clusters.sharedMemoryPort.max }}',
+                    tip: `Local port to accept shared memory connections<br/>If set to <b>-1</b> shared memory communication will be disabled`
+                })(
+                    pc-not-in-collection='::$ctrl.Clusters.sharedMemoryPort.invalidValues'
+                )
+                    +form-field__error({ error: 'notInCollection', message: 'Shared memory port should be more than "{{ ::$ctrl.Clusters.sharedMemoryPort.invalidValues[0] }}" or equal to "{{ ::$ctrl.Clusters.sharedMemoryPort.min }}"' })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Idle connection timeout:',
+                    model: `${communication}.idleConnectionTimeout`,
+                    name: '"idleConnectionTimeout"',
+                    placeholder: '30000',
+                    min: '1',
+                    tip: 'Maximum idle connection timeout upon which a connection to client will be closed'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Connect timeout:',
+                    model: `${communication}.connectTimeout`,
+                    name: '"connectTimeout"',
+                    placeholder: '5000',
+                    min: '0',
+                    tip: 'Connect timeout used when establishing connection with remote nodes'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Max. connect timeout:',
+                    model: `${communication}.maxConnectTimeout`,
+                    name: '"maxConnectTimeout"',
+                    placeholder: '600000',
+                    min: '0',
+                    tip: 'Maximum connect timeout'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Reconnect count:',
+                    model: `${communication}.reconnectCount`,
+                    name: '"comReconnectCount"',
+                    placeholder: '10',
+                    min: '1',
+                    tip: 'Maximum number of reconnect attempts used when establishing connection with remote nodes'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Socket send buffer:',
+                    model: `${communication}.socketSendBuffer`,
+                    name: '"socketSendBuffer"',
+                    placeholder: '32768',
+                    min: '0',
+                    tip: 'Send buffer size for sockets created or accepted by this SPI'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Socket receive buffer:',
+                    model: `${communication}.socketReceiveBuffer`,
+                    name: '"socketReceiveBuffer"',
+                    placeholder: '32768',
+                    min: '0',
+                    tip: 'Receive buffer size for sockets created or accepted by this SPI'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Slow client queue limit:',
+                    model: `${communication}.slowClientQueueLimit`,
+                    name: '"slowClientQueueLimit"',
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Slow client queue limit'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Ack send threshold:',
+                    model: `${communication}.ackSendThreshold`,
+                    name: '"ackSendThreshold"',
+                    placeholder: '{{ ::$ctrl.Clusters.ackSendThreshold.default }}',
+                    min: '{{ ::$ctrl.Clusters.ackSendThreshold.min }}',
+                    tip: 'Number of received messages per connection to node after which acknowledgment message is sent'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Message queue limit:',
+                    model: `${communication}.messageQueueLimit`,
+                    name: '"messageQueueLimit"',
+                    placeholder: '{{ ::$ctrl.Clusters.messageQueueLimit.default }}',
+                    min: '{{ ::$ctrl.Clusters.messageQueueLimit.min }}',
+                    tip: 'Message queue limit for incoming and outgoing messages'
+                })
+            .pc-form-grid-col-30
+                //- allowInvalid: true prevents from infinite digest loop when old value was 0 and becomes less than allowed minimum
+                +form-field__number({
+                    label: 'Unacknowledged messages:',
+                    model: `${communication}.unacknowledgedMessagesBufferSize`,
+                    name: '"unacknowledgedMessagesBufferSize"',
+                    placeholder: '{{ ::$ctrl.Clusters.unacknowledgedMessagesBufferSize.default }}',
+                    min: `{{ $ctrl.Clusters.unacknowledgedMessagesBufferSize.min(
+                        ${communication}.unacknowledgedMessagesBufferSize,
+                        ${communication}.messageQueueLimit,
+                        ${communication}.ackSendThreshold
+                    ) }}`,
+                    tip: `Maximum number of stored unacknowledged messages per connection to node<br/>
+                    If specified non zero value it should be
+                    <ul>
+                        <li>At least ack send threshold * {{ ::$ctrl.Clusters.unacknowledgedMessagesBufferSize.validRatio }}</li>
+                        <li>At least message queue limit * {{ ::$ctrl.Clusters.unacknowledgedMessagesBufferSize.validRatio }}</li>
+                    </ul>`
+                })(
+                    ng-model-options=`{
+                        allowInvalid: true
+                    }`
+                )
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Socket write timeout:',
+                    model: `${communication}.socketWriteTimeout`,
+                    name: '"socketWriteTimeout"',
+                    placeholder: '2000',
+                    min: '0',
+                    tip: 'Socket write timeout'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Selectors count:',
+                    model: `${communication}.selectorsCount`,
+                    name: '"selectorsCount"',
+                    placeholder: 'min(4, availableProcessors)',
+                    min: '1',
+                    tip: 'Count of selectors te be used in TCP server'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Selectors spins:',
+                    model: `${communication}.selectorSpins`,
+                    name: '"selectorSpins"',
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Defines how many non-blocking selector.selectNow() should be made before falling into selector.select(long) in NIO server'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Connections per node:',
+                    model: `${communication}.connectionsPerNode`,
+                    name: '"connectionsPerNode"',
+                    placeholder: '1',
+                    min: '1',
+                    tip: 'Number of connections to each remote node'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Address resolver:',
+                    model: `${communication}.addressResolver`,
+                    name: '"comAddressResolver"',
+                    tip: 'Provides resolution between external and internal addresses'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Direct buffer',
+                    model: `${communication}.directBuffer`,
+                    name: '"directBuffer"',
+                    tip: 'If value is true, then SPI will use ByteBuffer.allocateDirect(int) call<br/>\
+                          Otherwise, SPI will use ByteBuffer.allocate(int) call'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Direct send buffer',
+                    model: `${communication}.directSendBuffer`,
+                    name: '"directSendBuffer"',
+                    tip: 'Flag defining whether direct send buffer should be used'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'TCP_NODELAY option',
+                    model: `${communication}.tcpNoDelay`,
+                    name: '"tcpNoDelay"',
+                    tip: 'Value for TCP_NODELAY socket option'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Use paired connections',
+                    model: `${communication}.usePairedConnections`,
+                    name: '"usePairedConnections"',
+                    tip: 'Maintain connection for outgoing and incoming messages separately'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.3.0")')
+                +form-field__checkbox({
+                    label: 'Filter reachable addresses',
+                    model: `${communication}.filterReachableAddresses`,
+                    name: '"filterReachableAddresses"',
+                    tip: 'Filter for reachable addresses on creating tcp client'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterCommunication')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/connector.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/connector.pug
new file mode 100644
index 0000000..c04505d
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/connector.pug
@@ -0,0 +1,233 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'connector'
+-var model = '$ctrl.clonedCluster.connector'
+-var enabled = model + '.enabled'
+-var sslEnabled = enabled + ' && ' + model + '.sslEnabled'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Connector configuration
+    panel-description
+        | Configure HTTP REST configuration to enable HTTP server features.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/rest-api#general-configuration" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enabled',
+                    model: enabled,
+                    name: '"restEnabled"',
+                    tip: 'Flag indicating whether to configure connector configuration'
+                })
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'Jetty configuration path:',
+                    model: `${model}.jettyPath`,
+                    name: '"connectorJettyPath"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'Input path to Jetty configuration',
+                    tip: 'Path, either absolute or relative to IGNITE_HOME, to Jetty XML configuration file<br/>\
+                          Jetty is used to support REST over HTTP protocol for accessing Ignite APIs remotely<br/>\
+                          If not provided, Jetty instance with default configuration will be started picking IgniteSystemProperties.IGNITE_JETTY_HOST and IgniteSystemProperties.IGNITE_JETTY_PORT as host and port respectively'
+                })
+            .pc-form-grid-col-20
+                +form-field__ip-address({
+                    label:'TCP host:',
+                    model: `${model}.host`,
+                    name: '"connectorHost"',
+                    enabled: enabled,
+                    placeholder: 'IgniteConfiguration#getLocalHost()',
+                    tip: 'Host for TCP binary protocol server<br/>\
+                         This can be either an IP address or a domain name<br/>\
+                         If not defined, system - wide local address will be used IgniteConfiguration#getLocalHost()<br/>\
+                         You can also use "0.0.0.0" value to bind to all locally - available IP addresses'
+                })
+            .pc-form-grid-col-20
+                +form-field__number({
+                    label: 'TCP port:',
+                    model: `${model}.port`,
+                    name: '"connectorPort"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '11211',
+                    min: '1024',
+                    max: '65535',
+                    tip: 'Port for TCP binary protocol server'
+                })
+            .pc-form-grid-col-20
+                +form-field__number({
+                    label: 'TCP port range:',
+                    model: `${model}.portRange`,
+                    name: '"connectorPortRange"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '100',
+                    min: '1',
+                    tip: 'Number of ports for TCP binary protocol server to try if configured port is already in use'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Idle query cursor timeout:',
+                    model: `${model}.idleQueryCursorTimeout`,
+                    name: '"connectorIdleQueryCursorTimeout"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '600000',
+                    min: '0',
+                    tip: 'Reject open query cursors that is not used timeout<br/>\
+                          If no fetch query request come within idle timeout, it will be removed on next check for old query cursors'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Idle query cursor check frequency:',
+                    model: `${model}.idleQueryCursorCheckFrequency`,
+                    name: '"connectorIdleQueryCursorCheckFrequency"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '60000',
+                    min: '0',
+                    tip: 'Idle query cursors check frequency<br/>\
+                          This setting is used to reject open query cursors that is not used'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Idle timeout:',
+                    model: `${model}.idleTimeout`,
+                    name: '"connectorIdleTimeout"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '7000',
+                    min: '0',
+                    tip: 'Idle timeout for REST server<br/>\
+                          This setting is used to reject half - opened sockets<br/>\
+                          If no packets come within idle timeout, the connection is closed'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Receive buffer size:',
+                    model: `${model}.receiveBufferSize`,
+                    name: '"connectorReceiveBufferSize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '32768',
+                    min: '0',
+                    tip: 'REST TCP server receive buffer size'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Send buffer size:',
+                    model: `${model}.sendBufferSize`,
+                    name: '"connectorSendBufferSize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '32768',
+                    min: '0',
+                    tip: 'REST TCP server send buffer size'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Send queue limit:',
+                    model: `${model}.sendQueueLimit`,
+                    name: '"connectorSendQueueLimit"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'unlimited',
+                    min: '0',
+                    tip: 'REST TCP server send queue limit<br/>\
+                         If the limit exceeds, all successive writes will block until the queue has enough capacity'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Direct buffer',
+                    model: `${model}.directBuffer`,
+                    name: '"connectorDirectBuffer"',
+                    disabled: `!${enabled}`,
+                    tip: 'Flag indicating whether REST TCP server should use direct buffers<br/>\
+                          A direct buffer is a buffer that is allocated and accessed using native system calls, without using JVM heap<br/>\
+                          Enabling direct buffer may improve performance and avoid memory issues(long GC pauses due to huge buffer size)'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'TCP_NODELAY option',
+                    model: `${model}.noDelay`,
+                    name: '"connectorNoDelay"',
+                    disabled: `!${enabled}`,
+                    tip: 'Flag indicating whether TCP_NODELAY option should be set for accepted client connections<br/>\
+                          Setting this option reduces network latency and should be enabled in majority of cases<br/>\
+                          For more information, see Socket#setTcpNoDelay(boolean)'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Selector count:',
+                    model: `${model}.selectorCount`,
+                    name: '"connectorSelectorCount"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'min(4, availableProcessors)',
+                    min: '1',
+                    tip: 'Number of selector threads in REST TCP server<br/>\
+                          Higher value for this parameter may increase throughput, but also increases context switching'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Thread pool size:',
+                    model: `${model}.threadPoolSize`,
+                    name: '"connectorThreadPoolSize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'max(8, availableProcessors) * 2',
+                    min: '1',
+                    tip: 'Thread pool size to use for processing of client messages (REST requests)'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Message interceptor:',
+                    model: `${model}.messageInterceptor`,
+                    name: '"connectorMessageInterceptor"',
+                    disabled: `!(${enabled})`,
+                    tip: 'Interceptor allows to transform all objects exchanged via REST protocol<br/>\
+                         For example if you use custom serialisation on client you can write interceptor to transform binary representations received from client to Java objects and later access them from java code directly'
+                })
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'Secret key:',
+                    model: `${model}.secretKey`,
+                    name: '"connectorSecretKey"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'Specify to enable authentication',
+                    tip: 'Secret key to authenticate REST requests'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enable SSL',
+                    model: `${model}.sslEnabled`,
+                    name: '"connectorSslEnabled"',
+                    disabled: `!${enabled}`,
+                    tip: 'Enables/disables SSL for REST TCP binary protocol'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enable SSL client auth',
+                    model: `${model}.sslClientAuth`,
+                    name: '"connectorSslClientAuth"',
+                    disabled: `!(${sslEnabled})`,
+                    tip: 'Flag indicating whether or not SSL client authentication is required'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'SSL factory:',
+                    model: `${model}.sslFactory`,
+                    name: '"connectorSslFactory"',
+                    disabled: `!(${sslEnabled})`,
+                    required: sslEnabled,
+                    tip: 'Instance of Factory that will be used to create an instance of SSLContext for Secure Socket Layer on TCP binary protocol'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterConnector')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/data-storage.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/data-storage.pug
new file mode 100644
index 0000000..a3ec375
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/data-storage.pug
@@ -0,0 +1,515 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'dataStorageConfiguration'
+-var clusterModel = '$ctrl.clonedCluster'
+-var model = clusterModel + '.dataStorageConfiguration'
+-var dfltRegionModel = model + '.defaultDataRegionConfiguration'
+-var dataRegionConfigurations = model + '.dataRegionConfigurations'
+
+mixin data-region-form({modelAt, namePlaceholder, dataRegionsAt})
+    .pc-form-grid-col-60
+        +form-field__text({
+            label: 'Name:',
+            model: `${modelAt}.name`,
+            name: '"name"',
+            placeholder: namePlaceholder,
+        })(
+            ng-model-options='{allowInvalid: true}'
+
+            pc-not-in-collection='::$ctrl.Clusters.dataRegion.name.invalidValues'
+            ignite-unique=dataRegionsAt
+            ignite-unique-property='name'
+            ignite-unique-skip=`["_id", ${modelAt}]`
+        )
+            +form-field__error({ error: 'notInCollection', message: '{{::$ctrl.Clusters.dataRegion.name.invalidValues[0]}} is reserved for internal use' })
+            +form-field__error({ error: 'igniteUnique', message: 'Name should be unique' })
+
+    .pc-form-grid-col-30
+        form-field-size(
+            label='Initial size:'
+            ng-model=`${modelAt}.initialSize`
+            name='initialSize'
+            placeholder='{{ $ctrl.Clusters.dataRegion.initialSize.default / _drISScale.value }}'
+            min='{{ ::$ctrl.Clusters.dataRegion.initialSize.min }}'
+            on-scale-change='_drISScale = $event'
+        )
+
+    .pc-form-grid-col-30
+        form-field-size(
+            ng-model=`${modelAt}.maxSize`
+            ng-model-options='{allowInvalid: true}'
+            name='maxSize'
+            label='Maximum size:'
+            placeholder='{{ ::$ctrl.Clusters.dataRegion.maxSize.default }}'
+            min=`{{ $ctrl.Clusters.dataRegion.maxSize.min(${modelAt}) }}`
+        )
+
+    .pc-form-grid-col-60(ng-if=`!${modelAt}.persistenceEnabled || ${modelAt}.swapPath`)
+        +form-field__text({
+            label: 'Swap file path:',
+            model: `${modelAt}.swapPath`,
+            name: '"swapPath"',
+            placeholder: 'Input swap file path',
+            tip: 'An optional path to a memory mapped file for this data region'
+        })
+
+    .pc-form-grid-col-60
+        +form-field__number({
+            label: 'Checkpoint page buffer:',
+            model: `${modelAt}.checkpointPageBufferSize`,
+            name: '"checkpointPageBufferSize"',
+            placeholder: '0',
+            min: '0',
+            tip: 'Amount of memory allocated for a checkpoint temporary buffer in bytes'
+        })
+
+    .pc-form-grid-col-60
+        +form-field__dropdown({
+            label: 'Entry versioning:',
+            model: `${modelAt}.pageEvictionMode`,
+            name: '"pageEvictionMode"',
+            placeholder: 'DISABLED',
+            options: '[\
+                {value: "DISABLED", label: "DISABLED"},\
+                {value: "RANDOM_LRU", label: "RANDOM_LRU"},\
+                {value: "RANDOM_2_LRU", label: "RANDOM_2_LRU"}\
+            ]',
+            tip: `An algorithm for memory pages eviction
+                <ul>
+                    <li>DISABLED - Eviction is disabled</li>
+                    <li>RANDOM_LRU - Once a memory region defined by a data region is configured, an off-heap array is allocated to track last usage timestamp for every individual data page</li>
+                    <li>RANDOM_2_LRU - Differs from Random - LRU only in a way that two latest access timestamps are stored for every data page</li>
+                </ul>`
+        })
+
+    .pc-form-grid-col-30
+        +form-field__number({
+            label: 'Eviction threshold:',
+            model: `${modelAt}.evictionThreshold`,
+            name: '"evictionThreshold"',
+            placeholder: '{{ ::$ctrl.Clusters.dataRegion.evictionThreshold.default }}',
+            min: '{{ ::$ctrl.Clusters.dataRegion.evictionThreshold.min }}',
+            max: '{{ ::$ctrl.Clusters.dataRegion.evictionThreshold.max }}',
+            step: '{{ ::$ctrl.Clusters.dataRegion.evictionThreshold.step }}',
+            tip: 'A threshold for memory pages eviction initiation'
+        })
+
+    .pc-form-grid-col-30
+        +form-field__number({
+            label: 'Empty pages pool size:',
+            model: `${modelAt}.emptyPagesPoolSize`,
+            name: '"emptyPagesPoolSize"',
+            placeholder: '{{ ::$ctrl.Clusters.dataRegion.emptyPagesPoolSize.default }}',
+            min: '{{ ::$ctrl.Clusters.dataRegion.emptyPagesPoolSize.min }}',
+            max: `{{ $ctrl.Clusters.dataRegion.emptyPagesPoolSize.max($ctrl.clonedCluster, ${modelAt}) }}`,
+            tip: 'The minimal number of empty pages to be present in reuse lists for this data region'
+        })
+
+    .pc-form-grid-col-30
+        +form-field__number({
+            label: 'Metrics sub interval count:',
+            model: `${modelAt}.metricsSubIntervalCount`,
+            name: '"metricsSubIntervalCount"',
+            placeholder: '{{ ::$ctrl.Clusters.dataRegion.metricsSubIntervalCount.default }}',
+            min: '{{ ::$ctrl.Clusters.dataRegion.metricsSubIntervalCount.min }}',
+            step: '{{ ::$ctrl.Clusters.dataRegion.metricsSubIntervalCount.step }}',
+            tip: 'A number of sub-intervals the whole rate time interval will be split into to calculate allocation and eviction rates'
+        })
+
+    .pc-form-grid-col-30
+        form-field-size(
+            ng-model=`${modelAt}.metricsRateTimeInterval`
+            ng-model-options='{allowInvalid: true}'
+            name='metricsRateTimeInterval'
+            size-type='seconds'
+            label='Metrics rate time interval:'
+            placeholder='{{ $ctrl.Clusters.dataRegion.metricsRateTimeInterval.default / _metricsRateTimeIntervalScale.value }}'
+            min=`{{ ::$ctrl.Clusters.dataRegion.metricsRateTimeInterval.min }}`
+            tip='Time interval for allocation rate and eviction rate monitoring purposes'
+            on-scale-change='_metricsRateTimeIntervalScale = $event'
+            size-scale-label='s'
+        )
+
+    .pc-form-grid-col-60
+        +form-field__checkbox({
+            label: 'Metrics enabled',
+            model: `${modelAt}.metricsEnabled`,
+            name: '"MemoryPolicyMetricsEnabled"',
+            tip: 'Whether memory metrics are enabled by default on node startup'
+        })
+
+    .pc-form-grid-col-60(ng-if=`!${modelAt}.swapPath`)
+        +form-field__checkbox({
+            label: 'Persistence enabled',
+            model: `${modelAt}.persistenceEnabled`,
+            name: '"RegionPersistenceEnabled" + $index',
+            tip: 'Enable Ignite Native Persistence'
+        })
+
+panel-collapsible(ng-show='$ctrl.available("2.3.0")' ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Data storage configuration
+    panel-description
+        | Page memory is a manageable off-heap based memory architecture that is split into pages of fixed size.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/distributed-persistent-store" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`$ctrl.available("2.3.0") && ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-30
+                +form-field__dropdown({
+                    label: 'Page size:',
+                    model: `${model}.pageSize`,
+                    name: '"DataStorageConfigurationPageSize"',
+                    options: `$ctrl.Clusters.dataStorageConfiguration.pageSize.values`,
+                    tip: 'Every memory region is split on pages of fixed size'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Concurrency level:',
+                    model: model + '.concurrencyLevel',
+                    name: '"DataStorageConfigurationConcurrencyLevel"',
+                    placeholder: 'availableProcessors',
+                    min: '2',
+                    tip: 'The number of concurrent segments in Ignite internal page mapping tables'
+                })
+            .pc-form-grid-col-60.pc-form-group__text-title
+                span System region
+            .pc-form-group.pc-form-grid-row
+                .pc-form-grid-col-30
+                    form-field-size(
+                        label='Initial size:'
+                        ng-model=`${model}.systemRegionInitialSize`
+                        name='DataStorageSystemRegionInitialSize'
+                        placeholder='{{ $ctrl.Clusters.dataStorageConfiguration.systemRegionInitialSize.default / systemRegionInitialSizeScale.value }}'
+                        min='{{ ::$ctrl.Clusters.dataStorageConfiguration.systemRegionInitialSize.min }}'
+                        tip='Initial size of a data region reserved for system cache'
+                        on-scale-change='systemRegionInitialSizeScale = $event'
+                    )
+                .pc-form-grid-col-30
+                    form-field-size(
+                        label='Max size:'
+                        ng-model=`${model}.systemRegionMaxSize`
+                        name='DataStorageSystemRegionMaxSize'
+                        placeholder='{{ $ctrl.Clusters.dataStorageConfiguration.systemRegionMaxSize.default / systemRegionMaxSizeScale.value }}'
+                        min='{{ $ctrl.Clusters.dataStorageConfiguration.systemRegionMaxSize.min($ctrl.clonedCluster) }}'
+                        tip='Maximum data region size reserved for system cache'
+                        on-scale-change='systemRegionMaxSizeScale = $event'
+                    )
+            .pc-form-grid-col-60.pc-form-group__text-title
+                span Default data region
+            .pc-form-group.pc-form-grid-row
+                +data-region-form({
+                    modelAt: dfltRegionModel,
+                    namePlaceholder: '{{ ::$ctrl.Clusters.dataRegion.name.default }}',
+                    dataRegionsAt: dataRegionConfigurations
+                })
+            .pc-form-grid-col-60
+                .ignite-form-field
+                    +form-field__label({ label: 'Data region configurations' })
+
+                    list-editable.pc-list-editable-with-form-grid(
+                        name='dataRegionConfigurations'
+                        ng-model=dataRegionConfigurations
+                    )
+                        list-editable-item-edit.pc-form-grid-row
+                            - form = '$parent.form'
+                            +data-region-form({
+                                modelAt: '$item',
+                                namePlaceholder: 'Data region name',
+                                dataRegionsAt: dataRegionConfigurations
+                            })
+                            - form = 'dataStorageConfiguration'
+                        list-editable-no-items
+                            list-editable-add-item-button(
+                                add-item=`$ctrl.Clusters.addDataRegionConfiguration($ctrl.clonedCluster)`
+                                label-single='data region configuration'
+                                label-multiple='data region configurations'
+                            )
+
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'Storage path:',
+                    model: `${model}.storagePath`,
+                    name: '"DataStoragePath"',
+                    placeholder: 'db',
+                    tip: 'Directory where index and partition files are stored'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Checkpoint frequency:',
+                    model: `${model}.checkpointFrequency`,
+                    name: '"DataStorageCheckpointFrequency"',
+                    placeholder: '180000',
+                    min: '1',
+                    tip: 'Frequency which is a minimal interval when the dirty pages will be written to the Persistent Store'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.7.0")')
+                +form-field__number({
+                    label: 'Checkpoint read lock timeout:',
+                    model: `${model}.checkpointReadLockTimeout`,
+                    name: '"DataStorageCheckpointReadLockTimeout"',
+                    placeholder: 'System workers blocked timeout',
+                    min: '1',
+                    tip: 'Timeout for checkpoint read lock acquisition'
+                })
+            .pc-form-grid-col-20
+                +form-field__number({
+                    label: 'Checkpoint threads:',
+                    model: `${model}.checkpointThreads`,
+                    name: '"DataStorageCheckpointThreads"',
+                    placeholder: '4',
+                    min: '1',
+                    tip: 'A number of threads to use for the checkpoint purposes'
+                })
+            .pc-form-grid-col-20
+                +form-field__dropdown({
+                    label: 'Checkpoint write order:',
+                    model: `${model}.checkpointWriteOrder`,
+                    name: '"DataStorageCheckpointWriteOrder"',
+                    placeholder: 'SEQUENTIAL',
+                    options: '[\
+                        {value: "RANDOM", label: "RANDOM"},\
+                        {value: "SEQUENTIAL", label: "SEQUENTIAL"}\
+                    ]',
+                    tip: 'Order of writing pages to disk storage during checkpoint.\
+                        <ul>\
+                            <li>RANDOM - Pages are written in order provided by checkpoint pages collection iterator</li>\
+                            <li>SEQUENTIAL - All checkpoint pages are collected into single list and sorted by page index</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-20
+                +form-field__dropdown({
+                    label: 'WAL mode:',
+                    model: `${model}.walMode`,
+                    name: '"DataStorageWalMode"',
+                    placeholder: 'DEFAULT',
+                    options: '[\
+                        {value: "DEFAULT", label: "DEFAULT"},\
+                        {value: "LOG_ONLY", label: "LOG_ONLY"},\
+                        {value: "BACKGROUND", label: "BACKGROUND"},\
+                        {value: "NONE", label: "NONE"}\
+                    ]',
+                    tip: 'Type define behavior wal fsync.\
+                        <ul>\
+                            <li>DEFAULT - full-sync disk writes</li>\
+                            <li>LOG_ONLY - flushes application buffers</li>\
+                            <li>BACKGROUND - does not force application&#39;s buffer flush</li>\
+                            <li>NONE - WAL is disabled</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'WAL path:',
+                    model: `${model}.walPath`,
+                    name: '"DataStorageWalPath"',
+                    placeholder: 'db/wal',
+                    tip: 'A path to the directory where WAL is stored'
+                })
+            .pc-form-grid-col-20
+                +form-field__number({
+                    label: 'WAL segments:',
+                    model: `${model}.walSegments`,
+                    name: '"DataStorageWalSegments"',
+                    placeholder: '10',
+                    min: '1',
+                    tip: 'A number of WAL segments to work with'
+                })
+            .pc-form-grid-col-20
+                +form-field__number({
+                    label: 'WAL segment size:',
+                    model: `${model}.walSegmentSize`,
+                    name: '"DataStorageWalSegmentSize"',
+                    placeholder: '67108864',
+                    min: '0',
+                    tip: 'Size of a WAL segment'
+                })
+            .pc-form-grid-col-20
+                +form-field__number({
+                    label: 'WAL history size:',
+                    model: `${model}.walHistorySize`,
+                    name: '"DataStorageWalHistorySize"',
+                    placeholder: '20',
+                    min: '1',
+                    tip: 'A total number of checkpoints to keep in the WAL history'
+                })
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'WAL archive path:',
+                    model: `${model}.walArchivePath`,
+                    name: '"DataStorageWalArchivePath"',
+                    placeholder: 'db/wal/archive',
+                    tip: 'A path to the WAL archive directory'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.7.0")')
+                form-field-size(
+                    label='Max allowed size of WAL archives:'
+                    ng-model=`${model}.maxWalArchiveSize`
+                    name='DataStorageMaxWalArchiveSize'
+                    placeholder='1'
+                    min='1'
+                    tip='Max allowed size of WAL archives'
+                    size-scale-label='gb'
+                    size-type='bytes'
+                )
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.7.0")')
+                +form-field__number({
+                    label: 'WAL compaction level:',
+                    model: `${model}.walCompactionLevel`,
+                    name: '"DataStorageWalCompactionLevel"',
+                    placeholder: '1',
+                    min: '0',
+                    max: '9',
+                    tip: 'ZIP level to WAL compaction (0-9)'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'WAL auto archive after inactivity:',
+                    model: `${model}.walAutoArchiveAfterInactivity`,
+                    name: '"DataStorageWalAutoArchiveAfterInactivity"',
+                    placeholder: '-1',
+                    min: '-1',
+                    tip: 'Time in millis to run auto archiving segment after last record logging'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.4.0")')
+                +form-field__number({
+                    label: 'WAL buffer size:',
+                    model: `${model}.walBufferSize`,
+                    name: '"DataStorageWalBufferSize"',
+                    placeholder: 'WAL segment size / 4',
+                    min: '1',
+                    tip: 'Size of WAL buffer'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'WAL flush frequency:',
+                    model: `${model}.walFlushFrequency`,
+                    name: '"DataStorageWalFlushFrequency"',
+                    placeholder: '2000',
+                    min: '1',
+                    tip: 'How often will be fsync, in milliseconds. In background mode, exist thread which do fsync by timeout'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'WAL fsync delay:',
+                    model: `${model}.walFsyncDelayNanos`,
+                    name: '"DataStorageWalFsyncDelay"',
+                    placeholder: '1000',
+                    min: '1',
+                    tip: 'WAL fsync delay, in nanoseconds'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'WAL record iterator buffer size:',
+                    model: `${model}.walRecordIteratorBufferSize`,
+                    name: '"DataStorageWalRecordIteratorBufferSize"',
+                    placeholder: '67108864',
+                    min: '1',
+                    tip: 'How many bytes iterator read from disk(for one reading), during go ahead WAL'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Lock wait time:',
+                    model: `${model}.lockWaitTime`,
+                    name: '"DataStorageLockWaitTime"',
+                    placeholder: '10000',
+                    min: '1',
+                    tip: 'Time out in milliseconds, while wait and try get file lock for start persist manager'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'WAL thread local buffer size:',
+                    model: `${model}.walThreadLocalBufferSize`,
+                    name: '"DataStorageWalThreadLocalBufferSize"',
+                    placeholder: '131072',
+                    min: '1',
+                    tip: 'Define size thread local buffer. Each thread which write to WAL have thread local buffer for serialize recode before write in WAL'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Metrics sub interval count:',
+                    model: `${model}.metricsSubIntervalCount`,
+                    name: '"DataStorageMetricsSubIntervalCount"',
+                    placeholder: '5',
+                    min: '1',
+                    tip: 'Number of sub - intervals the whole rate time interval will be split into to calculate rate - based metrics'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Metrics rate time interval:',
+                    model: `${model}.metricsRateTimeInterval`,
+                    name: '"DataStorageMetricsRateTimeInterval"',
+                    placeholder: '60000',
+                    min: '1000',
+                    tip: 'The length of the time interval for rate - based metrics. This interval defines a window over which hits will be tracked'
+                })
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'File IO factory:',
+                    model: `${model}.fileIOFactory`,
+                    name: '"DataStorageFileIOFactory"',
+                    placeholder: 'Default',
+                    options: '[\
+                        {value: "RANDOM", label: "RANDOM"},\
+                        {value: "ASYNC", label: "ASYNC"},\
+                        {value: null, label: "Default"},\
+                    ]',
+                    tip: 'Order of writing pages to disk storage during checkpoint.\
+                        <ul>\
+                            <li>RANDOM - Pages are written in order provided by checkpoint pages collection iterator</li>\
+                            <li>SEQUENTIAL - All checkpoint pages are collected into single list and sorted by page index</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Metrics enabled',
+                    model: `${model}.metricsEnabled`,
+                    name: '"DataStorageMetricsEnabled"',
+                    tip: 'Flag indicating whether persistence metrics collection is enabled'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Always write full pages',
+                    model: `${model}.alwaysWriteFullPages`,
+                    name: '"DataStorageAlwaysWriteFullPages"',
+                    tip: 'Flag indicating whether always write full pages'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Write throttling enabled',
+                    model: `${model}.writeThrottlingEnabled`,
+                    name: '"DataStorageWriteThrottlingEnabled"',
+                    tip: 'Throttle threads that generate dirty pages too fast during ongoing checkpoint'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.4.0")')
+                +form-field__checkbox({
+                    label: 'Enable WAL compaction',
+                    model: `${model}.walCompactionEnabled`,
+                    name: '"DataStorageWalCompactionEnabled"',
+                    tip: 'If true, system filters and compresses WAL archive in background'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.5.0")')
+                +form-field__checkbox({
+                    label: 'Authentication enabled',
+                    model: `${clusterModel}.authenticationEnabled`,
+                    name: '"authenticationEnabled"',
+                    disabled: `!$ctrl.Clusters.persistenceEnabled(${model})`,
+                    tip: 'Enable user authentication for cluster with persistence'
+                })
+
+        .pca-form-column-6
+            +preview-xml-java(clusterModel, 'clusterDataStorageConfiguration')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/deployment.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/deployment.pug
new file mode 100644
index 0000000..f7273ec
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/deployment.pug
@@ -0,0 +1,265 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'deployment'
+-var model = '$ctrl.clonedCluster'
+-var modelDeployment = '$ctrl.clonedCluster.deploymentSpi'
+-var exclude = model + '.peerClassLoadingLocalClassPathExclude'
+-var enabled = '$ctrl.clonedCluster.peerClassLoadingEnabled'
+-var uriListModel = modelDeployment + '.URI.uriList'
+-var scannerModel = modelDeployment + '.URI.scanners'
+-var uriDeployment = modelDeployment + '.kind === "URI"'
+-var localDeployment = modelDeployment + '.kind === "Local"'
+-var customDeployment = modelDeployment + '.kind === "Custom"'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Class deployment
+    panel-description
+        | Task and resources deployment in cluster.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/deployment-modes" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'Deployment mode:',
+                    model: `${model}.deploymentMode`,
+                    name: '"deploymentMode"',
+                    placeholder: 'SHARED',
+                    options: '[\
+                        {value: "PRIVATE", label: "PRIVATE"},\
+                        {value: "ISOLATED", label: "ISOLATED"}, \
+                        {value: "SHARED", label: "SHARED"},\
+                        {value: "CONTINUOUS", label: "CONTINUOUS"}\
+                    ]',
+                    tip: 'Task classes and resources sharing mode<br/>\
+                    The following deployment modes are supported:\
+                        <ul>\
+                            <li>PRIVATE - in this mode deployed classes do not share resources</li>\
+                            <li>ISOLATED - in this mode tasks or classes deployed within the same class loader will share the same instances of resources</li>\
+                            <li>SHARED - same as ISOLATED, but now tasks from different master nodes with the same user version and same class loader will share the same class loader on remote nodes</li>\
+                            <li>CONTINUOUS - same as SHARED deployment mode, but resources will not be undeployed even after all master nodes left grid</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enable peer class loading',
+                    model: `${model}.peerClassLoadingEnabled`,
+                    name: '"peerClassLoadingEnabled"',
+                    tip: 'Enables/disables peer class loading'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Missed resources cache size:',
+                    model: `${model}.peerClassLoadingMissedResourcesCacheSize`,
+                    name: '"peerClassLoadingMissedResourcesCacheSize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '100',
+                    min: '0',
+                    tip: 'If size greater than 0, missed resources will be cached and next resource request ignored<br/>\
+                          If size is 0, then request for the resource will be sent to the remote node every time this resource is requested'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Pool size:',
+                    model: `${model}.peerClassLoadingThreadPoolSize`,
+                    name: '"peerClassLoadingThreadPoolSize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '2',
+                    min: '1',
+                    tip: 'Thread pool size to use for peer class loading'
+                })
+            .pc-form-grid-col-60
+                mixin clusters-deployment-packages
+                    .ignite-form-field
+                        -let items = exclude
+
+                        list-editable(
+                            ng-model=items
+                            name='localClassPathExclude'
+                            list-editable-cols=`::[{
+                                name: "Local class path excludes:",
+                                tip: "List of packages from the system classpath that need to be peer-to-peer loaded from task originating node<br/>
+                                '*' is supported at the end of the package name which means that all sub-packages and their classes are included like in Java package import clause"
+                            }]`
+                            ng-disabled=enabledToDisabled(enabled)
+                        )
+                            list-editable-item-view {{ $item }}
+
+                            list-editable-item-edit
+                                +form-field__java-package({
+                                    label: 'Package name',
+                                    model: '$item',
+                                    name: '"packageName"',
+                                    placeholder: 'Enter package name',
+                                    required: enabled
+                                })(
+                                    ignite-unique=items
+                                    ignite-form-field-input-autofocus='true'
+                                )
+                                    +form-field__error({ error: 'igniteUnique', message: 'Such package already exists!' })
+
+                            list-editable-no-items
+                                list-editable-add-item-button(
+                                    add-item=`$editLast($ctrl.Clusters.addPeerClassLoadingLocalClassPathExclude(${model}))`
+                                    label-single='package'
+                                    label-multiple='packages'
+                                )
+
+                -var form = '$parent.form'
+                +clusters-deployment-packages
+                -var form = 'deployment'
+
+            //- Since ignite 2.0
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
+                +form-field__java-class({
+                    label: 'Class loader:',
+                    model: `${model}.classLoader`,
+                    name: '"classLoader"',
+                    tip: 'Loader which will be used for instantiating execution context'
+                })
+
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'Deployment variant:',
+                    model: `${modelDeployment}.kind`,
+                    name: '"deploymentKind"',
+                    placeholder: 'Default',
+                    options: '[\
+                        {value: "URI", label: "URI"},\
+                        {value: "Local", label: "Local"}, \
+                        {value: "Custom", label: "Custom"},\
+                        {value: null, label: "Default"}\
+                    ]',
+                    tip: 'Grid deployment SPI is in charge of deploying tasks and classes from different sources:\
+                        <ul>\
+                            <li>URI - Deploy tasks from different sources like file system folders, email and HTTP</li>\
+                            <li>Local - Only within VM deployment on local node</li>\
+                            <li>Custom - Custom implementation of DeploymentSpi</li>\
+                            <li>Default - Default configuration of LocalDeploymentSpi will be used</li>\
+                        </ul>'
+                })
+            .pc-form-group(ng-show=uriDeployment).pc-form-grid-row
+                .pc-form-grid-col-60
+                    mixin clusters-deployment-uri
+                        .ignite-form-field
+                            -let items = uriListModel
+
+                            list-editable(
+                                ng-model=items
+                                name='uriList'
+                                list-editable-cols=`::[{
+                                    name: "URI list:",
+                                    tip: "List of URI which point to GAR file and which should be scanned by SPI for the new tasks"
+                                }]`
+                            )
+                                list-editable-item-view {{ $item }}
+
+                                list-editable-item-edit
+                                    +list-url-field('URL', '$item', '"url"', items)
+                                        +form-field__error({ error: 'igniteUnique', message: 'Such URI already configured!' })
+
+                                list-editable-no-items
+                                    list-editable-add-item-button(
+                                        add-item=`$editLast((${items} = ${items} || []).push(''))`
+                                        label-single='URI'
+                                        label-multiple='URIs'
+                                    )
+
+                    - var form = '$parent.form'
+                    +clusters-deployment-uri
+                    - var form = 'deployment'
+
+                .pc-form-grid-col-60
+                    +form-field__text({
+                        label: 'Temporary directory path:',
+                        model: modelDeployment + '.URI.temporaryDirectoryPath',
+                        name: '"DeploymentURITemporaryDirectoryPath"',
+                        placeholder: 'Temporary directory path',
+                        tip: 'Absolute path to temporary directory which will be used by deployment SPI to keep all deployed classes in'
+                    })
+                .pc-form-grid-col-60
+                    mixin clusters-deployment-scanner
+                        .ignite-form-field
+                            -let items = scannerModel
+
+                            list-editable(
+                                ng-model=items
+                                name='scannerModel'
+                                list-editable-cols=`::[{name: "URI deployment scanners:"}]`
+                            )
+                                list-editable-item-view {{ $item }}
+
+                                list-editable-item-edit
+                                    +list-java-class-field('Scanner', '$item', '"scanner"', items)
+                                        +form-field__error({ error: 'igniteUnique', message: 'Such scanner already configured!' })
+
+                                list-editable-no-items
+                                    list-editable-add-item-button(
+                                        add-item=`$editLast((${items} = ${items} || []).push(''))`
+                                        label-single='scanner'
+                                        label-multiple='scanners'
+                                    )
+
+                    - var form = '$parent.form'
+                    +clusters-deployment-scanner
+                    - var form = 'deployment'
+
+                .pc-form-grid-col-60
+                    +form-field__java-class({
+                        label: 'Listener:',
+                        model: `${modelDeployment}.URI.listener`,
+                        name: '"DeploymentURIListener"',
+                        tip: 'Deployment event listener',
+                        validationActive: uriDeployment
+                    })
+                .pc-form-grid-col-60
+                    +form-field__checkbox({
+                        label: 'Check MD5',
+                        model: `${modelDeployment}.URI.checkMd5`,
+                        name: '"DeploymentURICheckMd5"',
+                        tip: 'Exclude files with same md5s from deployment'
+                    })
+                .pc-form-grid-col-60
+                    +form-field__checkbox({
+                        label: 'Encode URI',
+                        model: `${modelDeployment}.URI.encodeUri`,
+                        name: '"DeploymentURIEncodeUri"',
+                        tip: 'URI must be encoded before usage'
+                    })
+            .pc-form-group(ng-show=localDeployment).pc-form-grid-row
+                .pc-form-grid-col-60
+                    +form-field__java-class({
+                        label: 'Listener:',
+                        model: `${modelDeployment}.Local.listener`,
+                        name: '"DeploymentLocalListener"',
+                        tip: 'Deployment event listener',
+                        validationActive: localDeployment
+                    })
+            .pc-form-group(ng-show=customDeployment).pc-form-grid-row
+                .pc-form-grid-col-60
+                    +form-field__java-class({
+                        label: 'Class:',
+                        model: `${modelDeployment}.Custom.className`,
+                        name: '"DeploymentCustom"',
+                        required: customDeployment,
+                        tip: 'DeploymentSpi implementation class',
+                        validationActive: customDeployment
+                    })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterDeployment')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/discovery.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/discovery.pug
new file mode 100644
index 0000000..2d5b017
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/discovery.pug
@@ -0,0 +1,259 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'discovery'
+-var model = '$ctrl.clonedCluster.discovery'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Discovery
+    panel-description
+        | TCP/IP discovery configuration.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/cluster-discovery" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-20
+                +form-field__ip-address({
+                    label: 'Local address:',
+                    model: `${model}.localAddress`,
+                    name: '"discoLocalAddress"',
+                    enabled: 'true',
+                    placeholder: '228.1.2.4',
+                    tip: 'Local host IP address that discovery SPI uses<br/>\
+                         If not provided a first found non-loopback address will be used'
+                })
+            .pc-form-grid-col-20
+                +form-field__number({
+                    label: 'Local port:',
+                    model: `${model}.localPort`,
+                    name: '"discoLocalPort"',
+                    placeholder: '47500',
+                    min: '1024',
+                    max: '65535',
+                    tip: 'Local port which node uses'
+                })
+            .pc-form-grid-col-20
+                +form-field__number({
+                    label: 'Local port range:',
+                    model: `${model}.localPortRange`,
+                    name: '"discoLocalPortRange"',
+                    placeholder: '100',
+                    min: '1',
+                    tip: 'Local port range'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label:'Address resolver:',
+                    model: `${model}.addressResolver`,
+                    name: '"discoAddressResolver"',
+                    tip: 'Provides resolution between external and internal addresses'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Socket timeout:',
+                    model: `${model}.socketTimeout`,
+                    name: '"socketTimeout"',
+                    placeholder: '5000',
+                    min: '0',
+                    tip: 'Socket operations timeout'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Acknowledgement timeout:',
+                    model: `${model}.ackTimeout`,
+                    name: '"ackTimeout"',
+                    placeholder: '5000',
+                    min: '0',
+                    max: `{{ ${model}.maxAckTimeout || 600000 }}`,
+                    tip: 'Message acknowledgement timeout'
+                })
+                    +form-field__error({ error: 'max', message: `Acknowledgement timeout should be less than max acknowledgement timeout ({{ ${model}.maxAckTimeout || 60000 }}).` })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Max acknowledgement timeout:',
+                    model: `${model}.maxAckTimeout`,
+                    name: '"maxAckTimeout"',
+                    placeholder: '600000',
+                    min: '0',
+                    tip: 'Maximum message acknowledgement timeout'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Network timeout:',
+                    model: `${model}.networkTimeout`,
+                    name: '"discoNetworkTimeout"',
+                    placeholder: '5000',
+                    min: '1',
+                    tip: 'Timeout to use for network operations'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Join timeout:',
+                    model: `${model}.joinTimeout`,
+                    name: '"joinTimeout"',
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Join timeout<br/>' +
+                          '0 means wait forever'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Thread priority:',
+                    model: `${model}.threadPriority`,
+                    name: '"threadPriority"',
+                    placeholder: '10',
+                    min: '1',
+                    tip: 'Thread priority for all threads started by SPI'
+                })
+
+            //- Removed in ignite 2.0
+            .pc-form-grid-col-60(ng-if-start='$ctrl.available(["1.0.0", "2.0.0"])')
+                +form-field__number({
+                    label: 'Heartbeat frequency:',
+                    model: `${model}.heartbeatFrequency`,
+                    name: '"heartbeatFrequency"',
+                    placeholder: '2000',
+                    min: '1',
+                    tip: 'Heartbeat messages issuing frequency'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Max heartbeats miss w/o init:',
+                    model: `${model}.maxMissedHeartbeats`,
+                    name: '"maxMissedHeartbeats"',
+                    placeholder: '1',
+                    min: '1',
+                    tip: 'Max heartbeats count node can miss without initiating status check'
+                })
+            .pc-form-grid-col-30(ng-if-end)
+                +form-field__number({
+                    label: 'Max missed client heartbeats:',
+                    model: `${model}.maxMissedClientHeartbeats`,
+                    name: '"maxMissedClientHeartbeats"',
+                    placeholder: '5',
+                    min: '1',
+                    tip: 'Max heartbeats count node can miss without failing client node'
+                })
+
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Topology history:',
+                    model: `${model}.topHistorySize`,
+                    name: '"topHistorySize"',
+                    placeholder: '1000',
+                    min: '0',
+                    tip: 'Size of topology snapshots history'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Discovery listener:',
+                    model: `${model}.listener`,
+                    name: '"discoListener"',
+                    tip: 'Listener for grid node discovery events'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Data exchange:',
+                    model: `${model}.dataExchange`,
+                    name: '"dataExchange"',
+                    tip: 'Class name of handler for initial data exchange between Ignite nodes'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Metrics provider:',
+                    model: `${model}.metricsProvider`,
+                    name: '"metricsProvider"',
+                    tip: 'Class name of metric provider to discovery SPI'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Reconnect count:',
+                    model: `${model}.reconnectCount`,
+                    name: '"discoReconnectCount"',
+                    placeholder: '10',
+                    min: '1',
+                    tip: 'Reconnect attempts count'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Statistics frequency:',
+                    model: `${model}.statisticsPrintFrequency`,
+                    name: '"statisticsPrintFrequency"',
+                    placeholder: '0',
+                    min: '1',
+                    tip: 'Statistics print frequency'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'IP finder clean frequency:',
+                    model: `${model}.ipFinderCleanFrequency`,
+                    name: '"ipFinderCleanFrequency"',
+                    placeholder: '60000',
+                    min: '1',
+                    tip: 'IP finder clean frequency'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Node authenticator:',
+                    model: `${model}.authenticator`,
+                    name: '"authenticator"',
+                    tip: 'Class name of node authenticator implementation'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.4.0")')
+                +form-field__number({
+                    label: 'Reconnect delay:',
+                    model: `${model}.reconnectDelay`,
+                    name: '"reconnectDelay"',
+                    placeholder: '2000',
+                    min: '0',
+                    tip: 'Amount of time in milliseconds that node waits before retrying to (re)connect to the cluster'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.7.0")')
+                +form-field__number({
+                    label: 'Connection recovery timeout:',
+                    model: `${model}.connectionRecoveryTimeout`,
+                    name: '"connectionRecoveryTimeout"',
+                    placeholder: '10000',
+                    min: '0',
+                    tip: 'Defines how long server node would try to recovery connection'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.8.0")')
+                +form-field__number({
+                    label: 'SO Linger timeout:',
+                    model: `${model}.soLinger`,
+                    name: '"soLinger"',
+                    placeholder: '5',
+                    min: '-1',
+                    tip: 'SO_LINGER timeout for socket'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Force server mode',
+                    model: `${model}.forceServerMode`,
+                    name: '"forceServerMode"',
+                    tip: 'Force start TCP/IP discovery in server mode'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Client reconnect disabled',
+                    model: `${model}.clientReconnectDisabled`,
+                    name: '"clientReconnectDisabled"',
+                    tip: 'Disable try of client to reconnect after server detected client node failure'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterDiscovery')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/encryption.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/encryption.pug
new file mode 100644
index 0000000..102453c
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/encryption.pug
@@ -0,0 +1,81 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'encryption'
+-var model = '$ctrl.clonedCluster.encryptionSpi'
+
+panel-collapsible(ng-show='$ctrl.available("2.7.0")' ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Encryption
+    panel-description Encryption features for an Ignite
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'Encryption SPI:',
+                    model: `${model}.kind`,
+                    name: '"encryptionSpi"',
+                    placeholder: 'Disabled',
+                    options: '[\
+                            {value: null, label: "Disabled"},\
+                            {value: "Keystore", label: "Keystore"},\
+                            {value: "Custom", label: "Custom"}\
+                        ]',
+                    tip: 'Provides an ability to save an intermediate job state\
+                        <ul>\
+                            <li>Disabled - Encryption disabled</li>\
+                            <li>Keystore - Base on JDK provided cipher algorithm implementations</li>\
+                            <li>Custom - Custom encryption SPI implementation</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-60(ng-if-start=`${model}.kind === "Keystore"`)
+                +form-field__text({
+                    label: 'Key store path:',
+                    model: `${model}.Keystore.keyStorePath`,
+                    name: '"EncryptionKeyStorePath"',
+                    placeholder: 'Path to master key store file',
+                    tip: 'Path to master key store file'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Key size:',
+                    model: `${model}.Keystore.keySize`,
+                    name: '"EncryptionKeySize"',
+                    placeholder: '256',
+                    min: '1',
+                    tip: 'Encryption key size'
+                })
+            .pc-form-grid-col-60(ng-if-end)
+                +form-field__text({
+                    label: 'Master key name:',
+                    model: `${model}.Keystore.masterKeyName`,
+                    name: '"EncryptionMasterKeyName"',
+                    placeholder: 'ignite.master.key',
+                    tip: 'Mater key name'
+                })
+            .pc-form-grid-col-60(ng-if=`${model}.kind === "Custom"`)
+                +form-field__java-class({
+                    label: 'Class:',
+                    model: `${model}.Custom.className`,
+                    name: '"EncryptionClassName"',
+                    required: true,
+                    tip: 'Custom encryption SPI implementation class name',
+                    validationActive: true
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterEncryption')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/events.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/events.pug
new file mode 100644
index 0000000..12219fe
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/events.pug
@@ -0,0 +1,151 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'events'
+-var model = '$ctrl.clonedCluster'
+-var modelEventStorage = model + '.eventStorage'
+-var modelEventStorageKind = modelEventStorage + '.kind'
+-var eventStorageMemory = modelEventStorageKind + ' === "Memory"'
+-var eventStorageCustom = modelEventStorageKind + ' === "Custom"'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Events
+    panel-description
+        | Grid events are used for notification about what happens within the grid.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/events" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60(ng-if='$ctrl.available(["1.0.0", "2.0.0"])')
+                +form-field__dropdown({
+                    label: 'Event storage:',
+                    model: modelEventStorageKind,
+                    name: '"eventStorageKind"',
+                    placeholder: 'Disabled',
+                    options: '$ctrl.eventStorage',
+                    tip: 'Regulate how grid store events locally on node\
+                        <ul>\
+                            <li>Memory - All events are kept in the FIFO queue in-memory</li>\
+                            <li>Custom - Custom implementation of event storage SPI</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
+                +form-field__dropdown({
+                    label: 'Event storage:',
+                    model: modelEventStorageKind,
+                    name: '"eventStorageKind"',
+                    placeholder: 'Disabled',
+                    options: '$ctrl.eventStorage',
+                    tip: 'Regulate how grid store events locally on node\
+                        <ul>\
+                            <li>Memory - All events are kept in the FIFO queue in-memory</li>\
+                            <li>Custom - Custom implementation of event storage SPI</li>\
+                            <li>Disabled - Events are not collected</li>\
+                        </ul>'
+                })
+            .pc-form-group.pc-form-grid-row(ng-if=modelEventStorageKind)
+                .pc-form-grid-col-30(ng-if-start=eventStorageMemory)
+                    +form-field__number({
+                        label: 'Events expiration time:',
+                        model: `${modelEventStorage}.Memory.expireAgeMs`,
+                        name: '"writeBehindBatchSize"',
+                        placeholder: 'Long.MAX_VALUE',
+                        min: '1',
+                        tip: 'All events that exceed this value will be removed from the queue when next event comes'
+                    })
+                .pc-form-grid-col-30
+                    +form-field__number({
+                        label: 'Events queue size:',
+                        model: `${modelEventStorage}.Memory.expireCount`,
+                        name: '"EventStorageExpireCount"',
+                        placeholder: '10000',
+                        min: '1',
+                        tip: 'Events will be filtered out when new request comes'
+                    })
+                .pc-form-grid-col-60(ng-if-end)
+                    +form-field__java-class({
+                        label: 'Filter:',
+                        model: `${modelEventStorage}.Memory.filter`,
+                        name: '"EventStorageFilter"',
+                        tip: 'Filter for events to be recorded<br/>\
+                             Should be implementation of o.a.i.lang.IgnitePredicate&lt;o.a.i.events.Event&gt;',
+                        validationActive: eventStorageMemory
+                    })
+
+                .pc-form-grid-col-60(ng-if=eventStorageCustom)
+                    +form-field__java-class({
+                        label: 'Class:',
+                        model: `${modelEventStorage}.Custom.className`,
+                        name: '"EventStorageCustom"',
+                        required: eventStorageCustom,
+                        tip: 'Event storage implementation class name',
+                        validationActive: eventStorageCustom
+                    })
+
+                .pc-form-grid-col-60
+                    +form-field__dropdown({
+                        label: 'Include type:',
+                        model: `${model}.includeEventTypes`,
+                        name: '"includeEventTypes"',
+                        multiple: true,
+                        placeholder: 'Choose recorded event types',
+                        placeholderEmpty: '',
+                        options: '$ctrl.eventGroups',
+                        tip: 'Array of event types, which will be recorded by GridEventStorageManager#record(Event)<br/>\
+                             Note, that either the include event types or the exclude event types can be established'
+                    })
+            .pc-form-grid-col-60
+                .ignite-form-field
+                    +form-field__label({label: 'Local event listeners:', name: '"LocalEventListeners"'})
+                        +form-field__tooltip({title: `Local event listeners`})
+
+                    -var items = model + '.localEventListeners'
+                    list-editable.pc-list-editable-with-form-grid(ng-model=items name='LocalEventListeners')
+                        list-editable-item-edit.pc-form-grid-row
+                            - form = '$parent.form'
+                            .pc-form-grid-col-40
+                                +form-field__java-class({
+                                    label: 'Listener class name:',
+                                    model: '$item.className',
+                                    name: '"EventListenerClassName"',
+                                    required: true,
+                                    tip: 'Local event listener implementation class name',
+                                    validationActive: true
+                                })
+                            .pc-form-grid-col-20
+                                +form-field__dropdown({
+                                    label: 'Event types:',
+                                    model: '$item.eventTypes',
+                                    name: '"EventLisneterEventTypes"',
+                                    required: true,
+                                    multiple: true,
+                                    placeholder: 'Choose event types',
+                                    placeholderEmpty: '',
+                                    options: '$ctrl.eventTypes',
+                                    tip: 'Listened event types:'
+                                })
+                        list-editable-no-items
+                            list-editable-add-item-button(
+                                add-item=`$ctrl.Clusters.addLocalEventListener($ctrl.clonedCluster)`
+                                label-single='listener'
+                                label-multiple='listeners'
+                            )
+
+            - form = 'events'
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterEvents')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/failover.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/failover.pug
new file mode 100644
index 0000000..5320a80
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/failover.pug
@@ -0,0 +1,189 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var model = '$ctrl.clonedCluster'
+-var form = 'failoverSpi'
+-var failoverSpi = model + '.failoverSpi'
+-var failureHandler = model + '.failureHandler'
+-var failoverCustom = '$item.kind === "Custom"'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Failover configuration
+    panel-description
+        | Failover SPI provides ability to supply custom logic for handling failed execution of a grid job.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/fault-tolerance" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            //- Since ignite 2.0
+            .pc-form-grid-col-60(ng-if-start='$ctrl.available("2.0.0")')
+                +form-field__number({
+                    label: 'Failure detection timeout:',
+                    model: model + '.failureDetectionTimeout',
+                    name: '"failureDetectionTimeout"',
+                    placeholder: '10000',
+                    min: '1',
+                    tip: 'Failure detection timeout is used to determine how long the communication or discovery SPIs should wait before considering a remote connection failed'
+                })
+            .pc-form-grid-col-60(ng-if-end)
+                +form-field__number({
+                    label: 'Client failure detection timeout:',
+                    model: model + '.clientFailureDetectionTimeout',
+                    name: '"clientFailureDetectionTimeout"',
+                    placeholder: '30000',
+                    min: '1',
+                    tip: 'Failure detection timeout is used to determine how long the communication or discovery SPIs should wait before considering a remote connection failed'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.7.0")')
+                +form-field__number({
+                    label: 'System workers blocked timeout:',
+                    model: model + '.systemWorkerBlockedTimeout',
+                    name: '"SystemWorkerBlockedTimeout"',
+                    placeholder: 'Failure detection timeout',
+                    min: '1',
+                    tip: 'Maximum inactivity period for system worker'
+                })
+
+            .pc-form-grid-col-60
+                mixin clusters-failover-spi
+                    .ignite-form-field
+                        +form-field__label({ label: 'Failover SPI configurations:', name: '"failoverSpi"' })
+                            +form-field__tooltip({ title: `Failover SPI configurations` })
+                        -let items = failoverSpi
+
+                        list-editable.pc-list-editable-with-form-grid(ng-model=items name='failoverSpi')
+                            list-editable-item-edit.pc-form-grid-row
+                                .pc-form-grid-col-60
+                                    +form-field__dropdown({
+                                        required: true,
+                                        label: 'Failover SPI:',
+                                        model: '$item.kind',
+                                        name: '"failoverKind"',
+                                        placeholder: 'Choose Failover SPI',
+                                        options: '::$ctrl.Clusters.failoverSpis',
+                                        tip: `
+                                        Provides ability to supply custom logic for handling failed execution of a grid job
+                                        <ul>
+                                            <li>Job stealing - Supports job stealing from over-utilized nodes to under-utilized nodes</li>
+                                            <li>Never - Jobs are ordered as they arrived</li>
+                                            <li>Always - Jobs are first ordered by their priority</li>
+                                            <li>Custom - Jobs are activated immediately on arrival to mapped node</li>
+                                            <li>Default - Default FailoverSpi implementation</li>
+                                        </ul>`
+                                    })
+
+                                .pc-form-grid-col-60(ng-show='$item.kind === "JobStealing"')
+                                    +form-field__number({
+                                        label: 'Maximum failover attempts:',
+                                        model: '$item.JobStealing.maximumFailoverAttempts',
+                                        name: '"jsMaximumFailoverAttempts"',
+                                        placeholder: '5',
+                                        min: '0',
+                                        tip: 'Maximum number of attempts to execute a failed job on another node'
+                                    })
+                                .pc-form-grid-col-60(ng-show='$item.kind === "Always"')
+                                    +form-field__number({
+                                        label: 'Maximum failover attempts:',
+                                        model: '$item.Always.maximumFailoverAttempts',
+                                        name: '"alwaysMaximumFailoverAttempts"',
+                                        placeholder: '5',
+                                        min: '0',
+                                        tip: 'Maximum number of attempts to execute a failed job on another node'
+                                    })
+                                .pc-form-grid-col-60(ng-show=failoverCustom)
+                                    +form-field__java-class({
+                                        label: 'SPI implementation',
+                                        model: '$item.Custom.class',
+                                        name: '"failoverSpiClass"',
+                                        required: failoverCustom,
+                                        tip: 'Custom FailoverSpi implementation class name.',
+                                        validationActive: failoverCustom
+                                    })
+
+                            list-editable-no-items
+                                list-editable-add-item-button(
+                                    add-item=`(${items} = ${items} || []).push({})`
+                                    label-single='failover SPI'
+                                    label-multiple='failover SPIs'
+                                )
+
+                +clusters-failover-spi
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.5.0")')
+                +form-field__dropdown({
+                    label: 'Failure handler:',
+                    model: `${failureHandler}.kind`,
+                    name: '"FailureHandlerKind"',
+                    placeholder: 'Default',
+                    options: '$ctrl.failureHandlerVariant',
+                    tip: 'Handle failures<br/>\
+                        <ul>\
+                            <li>Restart process - Process will be terminated using Ignition.restart call</li>\
+                            <li>Try stop with timeout - Handler will try to stop node if tryStop value is true or terminate forcibly</li>\
+                            <li>Stop on critical error - Handler will stop node in case of critical error</li>\
+                            <li>Disabled - Ignores any failure</li>\n\
+                            <li>Custom - Custom implementation of failure handler</li>\
+                            <li>Default - Default implementation of failure handler</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-60(ng-if=`$ctrl.available("2.5.0") && ${failureHandler}.kind === "Custom"`)
+                +form-field__java-class({
+                    label: 'Class name:',
+                    model: `${failureHandler}.Custom.className`,
+                    name: '"CustomFailureHandler"',
+                    required: true,
+                    tip: 'Class name of custom failure handler implementation',
+                    validationActive: true
+                })
+            .pc-form-group.pc-form-grid-row(ng-if=`$ctrl.available("2.5.0") && ${failureHandler}.kind === 'StopNodeOnHalt'`)
+                .pc-form-grid-col-60
+                    +form-field__number({
+                        label: 'Stop node timeout:',
+                        model: `${failureHandler}.StopNodeOnHalt.timeout`,
+                        name: '"StopNodeOnHaltTimeout"',
+                        placeholder: '0',
+                        min: '0',
+                        tip: 'Timeout for forcibly terminating by using Runtime.getRuntime().halt()'
+                    })
+                .pc-form-grid-col-60
+                    +form-field__checkbox({
+                        label: 'Try to stop node',
+                        model: `${failureHandler}.StopNodeOnHalt.tryStop`,
+                        name: '"StopNodeOnHaltTryStop"',
+                        tip: 'Try to stop node'
+                    })
+            .pc-form-grid-col-60(ng-if=`$ctrl.available("2.5.0") && ['RestartProcess', 'StopNodeOnHalt', 'StopNode'].indexOf(${failureHandler}.kind) >= 0`)
+                +form-field__dropdown({
+                    label: 'Ignored failure types:',
+                    model: `${failureHandler}.ignoredFailureTypes`,
+                    name: '"FailureHandlerIgnoredFailureTypes"',
+                    multiple: true,
+                    placeholder: 'Choose ignored failure types',
+                    placeholderEmpty: '',
+                    options: '$ctrl.ignoredFailureTypes',
+                    tip: 'Ignored failure types:<br/>\
+                        <ul>\
+                            <li>SEGMENTATION - Node segmentation</li>\
+                            <li>SYSTEM_WORKER_TERMINATION - System worker termination</li>\
+                            <li>SYSTEM_WORKER_BLOCKED - System worker has not updated its heartbeat for a long time</li>\
+                            <li>CRITICAL_ERROR - Critical error - error which leads to the system\'s inoperability</li>\n\
+                            <li>SYSTEM_CRITICAL_OPERATION_TIMEOUT - System-critical operation has been timed out</li>\
+                        </ul>'
+                })
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterFailover')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general.pug
new file mode 100644
index 0000000..227dee0
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general.pug
@@ -0,0 +1,101 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'general'
+-var model = '$ctrl.clonedCluster'
+-var modelDiscoveryKind = model + '.discovery.kind'
+
+include ./general/discovery/cloud
+include ./general/discovery/google
+include ./general/discovery/jdbc
+include ./general/discovery/multicast
+include ./general/discovery/s3
+include ./general/discovery/shared
+include ./general/discovery/vm
+include ./general/discovery/zookeeper
+include ./general/discovery/kubernetes
+
+panel-collapsible(opened=`::true` ng-form=form)
+    panel-title General
+    panel-description
+        | Common cluster configuration.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/clustering" target="_blank") More info]
+    panel-content.pca-form-row
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-30
+                +form-field__text({
+                    label: 'Name:',
+                    model: `${model}.name`,
+                    name: '"clusterName"',
+                    placeholder: 'Input name',
+                    required: true,
+                    tip: 'Instance name allows to indicate to what grid this particular grid instance belongs to'
+                })(
+                    ignite-unique='$ctrl.shortClusters'
+                    ignite-unique-property='name'
+                    ignite-unique-skip=`["_id", ${model}]`
+                )
+                    +form-field__error({ error: 'igniteUnique', message: 'Cluster name should be unique.' })
+
+            .pc-form-grid-col-30
+                +form-field__ip-address({
+                    label: 'Local host:',
+                    model: `${model}.localHost`,
+                    name: '"localHost"',
+                    enabled: 'true',
+                    placeholder: '0.0.0.0',
+                    tip: 'System-wide local address or host for all Ignite components to bind to<br/>\
+                          If not defined then Ignite tries to use local wildcard address<br/>\
+                          That means that all services will be available on all network interfaces of the host machine'
+                })
+
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'Discovery:',
+                    model: `${model}.discovery.kind`,
+                    name: '"discovery"',
+                    placeholder: 'Choose discovery',
+                    options: '$ctrl.Clusters.discoveries',
+                    tip: 'Discovery allows to discover remote nodes in grid\
+                        <ul>\
+                            <li>Static IPs - IP Finder which works only with pre configured list of IP addresses specified</li>\
+                            <li>Multicast - Multicast based IP finder</li>\
+                            <li>AWS S3 - AWS S3 based IP finder that automatically discover cluster nodes on Amazon EC2 cloud</li>\
+                            <li>Apache jclouds - Apache jclouds multi cloud toolkit based IP finder for cloud platforms with unstable IP addresses</li>\
+                            <li>Google cloud storage - Google Cloud Storage based IP finder that automatically discover cluster nodes on Google Compute Engine cluster</li>\
+                            <li>JDBC - JDBC based IP finder that use database to store node IP address</li>\
+                            <li>Shared filesystem - Shared filesystem based IP finder that use file to store node IP address</li>\
+                            <li>Apache ZooKeeper - Apache ZooKeeper based IP finder when you use ZooKeeper to coordinate your distributed environment</li>\
+                            <li>Kubernetes - IP finder for automatic lookup of Ignite nodes running in Kubernetes environment</li>\
+                        </ul>'
+                })
+            .pc-form-group
+                +discovery-cloud()(ng-if=`${modelDiscoveryKind} === 'Cloud'`)
+                +discovery-google()(ng-if=`${modelDiscoveryKind} === 'GoogleStorage'`)
+                +discovery-jdbc()(ng-if=`${modelDiscoveryKind} === 'Jdbc'`)
+                +discovery-multicast()(ng-if=`${modelDiscoveryKind} === 'Multicast'`)
+                +discovery-s3()(ng-if=`${modelDiscoveryKind} === 'S3'`)
+                +discovery-shared()(ng-if=`${modelDiscoveryKind} === 'SharedFs'`)
+                +discovery-vm()(ng-if=`${modelDiscoveryKind} === 'Vm'`)
+                +discovery-zookeeper()(ng-if=`${modelDiscoveryKind} === 'ZooKeeper'`)
+                +discovery-kubernetes()(ng-if=`${modelDiscoveryKind} === 'Kubernetes'`)
+
+        .pca-form-column-6
+            -var model = '$ctrl.clonedCluster'
+            +preview-xml-java(model, 'clusterGeneral')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/cloud.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/cloud.pug
new file mode 100644
index 0000000..800302a
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/cloud.pug
@@ -0,0 +1,100 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+mixin discovery-cloud(modelAt='$ctrl.clonedCluster')
+
+    -const model = `${modelAt}.discovery.Cloud`
+    -const discoveryKind = 'Cloud'
+    -const required = `${modelAt}.discovery.kind == "${discoveryKind}"`
+    -const regions = `${model}.regions`
+    -const zones = `${model}.zones`
+    -const formRegions = 'discoveryCloudRegions'
+    -const formZones = 'discoveryCloudZones'
+
+    div.pc-form-grid-row&attributes(attributes=attributes)
+        .pc-form-grid-col-30
+            +form-field__text({
+                label: 'Credential:',
+                model: `${model}.credential`,
+                name: '"credential"',
+                placeholder: 'Input cloud credential',
+                tip: 'Credential that is used during authentication on the cloud<br/>\
+                      Depending on a cloud platform it can be a password or access key'
+            })
+        .pc-form-grid-col-30
+            +form-field__text({
+                label: 'Path to credential:',
+                model: `${model}.credentialPath`,
+                name: '"credentialPath"',
+                placeholder: 'Input path to credential',
+                tip: 'Path to a credential that is used during authentication on the cloud<br/>\
+                     Access key or private key should be stored in a plain or PEM file without a passphrase'
+            })
+        .pc-form-grid-col-30
+            +form-field__text({
+                label: 'Identity:',
+                model: `${model}.identity`,
+                name: '"' + discoveryKind + 'Identity"',
+                required: required,
+                placeholder: 'Input identity',
+                tip: 'Identity that is used as a user name during a connection to the cloud<br/>\
+                     Depending on a cloud platform it can be an email address, user name, etc'
+            })
+        .pc-form-grid-col-30
+            +form-field__text({
+                label:'Provider:',
+                model: `${model}.provider`,
+                name: '"' + discoveryKind + 'Provider"',
+                required: required,
+                placeholder: 'Input provider',
+                tip: 'Cloud provider to use'
+            })
+        .pc-form-grid-col-60
+            .ignite-form-field
+                +list-text-field({
+                    items: regions,
+                    lbl: 'Region name',
+                    name: 'regionName',
+                    itemName: 'region',
+                    itemsName: 'regions'
+                })(
+                    list-editable-cols=`::[{
+                        name: 'Regions:',
+                        tip: "List of regions where VMs are located<br />
+                        If the regions are not set then every region, that a cloud provider has, will be investigated. This could lead to significant performance degradation<br />
+                        Note, that some cloud providers, like Google Compute Engine, doesn't have a notion of a region. For such providers regions are redundant"
+                    }]`
+                )
+                    +form-field__error({ error: 'igniteUnique', message: 'Such region already exists!' })
+        .pc-form-grid-col-60
+            .ignite-form-field
+                +list-text-field({
+                    items: zones,
+                    lbl: 'Zone name',
+                    name: 'zoneName',
+                    itemName: 'zone',
+                    itemsName: 'zones'
+                })(
+                    list-editable-cols=`::[{
+                        name: 'Zones:',
+                        tip: "List of zones where VMs are located<br />
+                        If the zones are not set then every zone from specified regions, will be taken into account<br />
+                        Note, that some cloud providers, like Rackspace, doesn't have a notion of a zone. For such providers zones are redundant"
+                    }]`
+                )
+                    +form-field__error({ error: 'igniteUnique', message: 'Such zone already exists!' })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/google.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/google.pug
new file mode 100644
index 0000000..01996ac
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/google.pug
@@ -0,0 +1,63 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+mixin discovery-google(modelAt = '$ctrl.clonedCluster')
+    -const discoveryKind = 'GoogleStorage'
+    -const required = `${modelAt}.discovery.kind == '${discoveryKind}'`
+    -const model = `${modelAt}.discovery.GoogleStorage`
+
+    .pc-form-grid-row&attributes(attributes=attributes)
+        .pc-form-grid-col-30
+            +form-field__text({
+                label: 'Project name:',
+                model: `${model}.projectName`,
+                name: `'${discoveryKind}ProjectName'`,
+                required: required,
+                placeholder: 'Input project name',
+                tip: 'Google Cloud Platforms project name<br/>\
+                     Usually this is an auto generated project number(ex. 208709979073) that can be found in "Overview" section of Google Developer Console'
+            })
+        .pc-form-grid-col-30
+            +form-field__text({
+                label: 'Bucket name:',
+                model: `${model}.bucketName`,
+                name: `'${discoveryKind}BucketName'`,
+                required: required,
+                placeholder: 'Input bucket name',
+                tip: 'Google Cloud Storage bucket name<br/>\
+                     If the bucket does not exist Ignite will automatically create it<br/>\
+                     However the name must be unique across whole Google Cloud Storage and Service Account Id must be authorized to perform this operation'
+            })
+        .pc-form-grid-col-30
+            +form-field__text({
+                label: 'Private key path:',
+                model: `${model}.serviceAccountP12FilePath`,
+                name: `'${discoveryKind}ServiceAccountP12FilePath'`,
+                required: required,
+                placeholder: 'Input private key path',
+                tip: 'Full path to the private key in PKCS12 format of the Service Account'
+            })
+        .pc-form-grid-col-30
+            +form-field__text({
+                label: 'Account id:',
+                model: `${model}.serviceAccountId`,
+                name: `'${discoveryKind}ServiceAccountId'`,
+                required: required,
+                placeholder: 'Input account id',
+                tip: 'Service account ID (typically an e-mail address)'
+            })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/jdbc.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/jdbc.pug
new file mode 100644
index 0000000..e82814f
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/jdbc.pug
@@ -0,0 +1,52 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+mixin discovery-jdbc(modelAt = '$ctrl.clonedCluster')
+    -const model = `${modelAt}.discovery.Jdbc`
+    -const required = `${modelAt}.discovery.kind === "Jdbc"`
+
+    .pc-form-grid-row&attributes(attributes=attributes)
+        .pc-form-grid-col-30
+            +form-field__text({
+                label: 'Data source bean name:',
+                model: `${model}.dataSourceBean`,
+                name: '"dataSourceBean"',
+                required: required,
+                placeholder:'Input bean name',
+                tip: 'Name of the data source bean in Spring context'
+            })
+        .pc-form-grid-col-30
+            +form-field__dialect({
+                label: 'Dialect:',
+                model: `${model}.dialect`,
+                name: '"dialect"',
+                required,
+                tip: 'Dialect of SQL implemented by a particular RDBMS:',
+                genericDialectName: 'Generic JDBC dialect',
+                placeholder: 'Choose JDBC dialect'
+            })
+        .pc-form-grid-col-60
+            +form-field__checkbox({
+                label: 'DB schema should be initialized by Ignite',
+                model: `${model}.initSchema`,
+                name: '"initSchema"',
+                tip: 'Flag indicating whether DB schema should be initialized by Ignite or was explicitly created by user'
+            })
+        .pc-form-grid-col-30(ng-if=`$ctrl.Clusters.requiresProprietaryDrivers(${modelAt}.discovery.Jdbc)`)
+            a.link-success(ng-href=`{{ $ctrl.Clusters.jdbcDriverURL(${modelAt}.discovery.Jdbc) }}` target='_blank')
+                | Download JDBC drivers?
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/kubernetes.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/kubernetes.pug
new file mode 100644
index 0000000..32d94fc
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/kubernetes.pug
@@ -0,0 +1,59 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+mixin discovery-kubernetes(modelAt = '$ctrl.clonedCluster')
+    -const discoveryKind = 'Kubernetes'
+    -const model = `${modelAt}.discovery.Kubernetes`
+
+    .pc-form-grid-row&attributes(attributes=attributes)
+        .pc-form-grid-col-30
+            +form-field__text({
+                label: 'Service name:',
+                model: `${model}.serviceName`,
+                name: `'${discoveryKind}ServiceName'`,
+                placeholder: 'ignite',
+                tip: "The name of Kubernetes service for Ignite pods' IP addresses lookup.<br/>\
+                     The name of the service must be equal to the name set in service's Kubernetes configuration.<br/>\
+                     If this parameter is not changed then the name of the service has to be set to 'ignite' in the corresponding Kubernetes configuration."
+            })
+        .pc-form-grid-col-30
+            +form-field__text({
+                label: 'Namespace:',
+                model: `${model}.namespace`,
+                name: `'${discoveryKind}Namespace'`,
+                placeholder: 'default',
+                tip: "The namespace the Kubernetes service belongs to.<br/>\
+                      By default, it's supposed that the service is running under Kubernetes `default` namespace."
+            })
+        .pc-form-grid-col-60
+            +form-field__url({
+                label: 'Kubernetes server:',
+                model: `${model}.masterUrl`,
+                name: `'${discoveryKind}MasterUrl'`,
+                enabled: 'true',
+                placeholder: 'https://kubernetes.default.svc.cluster.local:443',
+                tip: 'The host name of the Kubernetes API server'
+            })
+        .pc-form-grid-col-60
+            +form-field__text({
+                label: 'Service token file:',
+                model: `${model}.accountToken`,
+                name: `'${discoveryKind}AccountToken'`,
+                placeholder: '/var/run/secrets/kubernetes.io/serviceaccount/token',
+                tip: 'The path to the service token file'
+            })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/multicast.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/multicast.pug
new file mode 100644
index 0000000..b767e9c
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/multicast.pug
@@ -0,0 +1,94 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+mixin discovery-multicast(modelAt = '$ctrl.clonedCluster')
+    -const model = `${modelAt}.discovery.Multicast`
+    -const addresses = `${model}.addresses`
+
+    .pc-form-grid-row&attributes(attributes=attributes)
+        .pc-form-grid-col-30
+            +form-field__ip-address({
+                label: 'IP address:',
+                model: `${model}.multicastGroup`,
+                name: '"multicastGroup"',
+                enabled: 'true',
+                placeholder: '228.1.2.4',
+                tip: 'IP address of multicast group'
+            })
+        .pc-form-grid-col-30
+            +form-field__number({
+                label: 'Port number:',
+                model: `${model}.multicastPort`,
+                name: '"multicastPort"',
+                placeholder: '47400',
+                min: '0',
+                max: '65535',
+                tip: 'Port number which multicast messages are sent to'
+            })
+        .pc-form-grid-col-20
+            +form-field__number({
+                label: 'Waits for reply:',
+                model: `${model}.responseWaitTime`,
+                name: '"responseWaitTime"',
+                placeholder: '500',
+                min: '0',
+                tip: 'Time in milliseconds IP finder waits for reply to multicast address request'
+            })
+        .pc-form-grid-col-20
+            +form-field__number({
+                label: 'Attempts count:',
+                model: `${model}.addressRequestAttempts`,
+                name: '"addressRequestAttempts"',
+                placeholder: '2',
+                min: '0',
+                tip: 'Number of attempts to send multicast address request<br/>\
+                     IP finder re - sends request only in case if no reply for previous request is received'
+            })
+        .pc-form-grid-col-20.pc-form-grid-col-free
+            +form-field__ip-address({
+                label: 'Local address:',
+                model: `${model}.localAddress`,
+                name: '"localAddress"',
+                enabled: 'true',
+                placeholder: '0.0.0.0',
+                tip: 'Local host address used by this IP finder<br/>\
+                     If provided address is non - loopback then multicast socket is bound to this interface<br/>\
+                     If local address is not set or is any local address then IP finder creates multicast sockets for all found non - loopback addresses'
+            })
+        .pc-form-grid-col-60
+            .ignite-form-field
+                +list-addresses({
+                    items: addresses,
+                    name: 'multicastAddresses',
+                    tip: `Addresses may be represented as follows:
+                    <ul>
+                        <li>IP address (e.g. 127.0.0.1, 9.9.9.9, etc)</li>
+                        <li>IP address and port (e.g. 127.0.0.1:47500, 9.9.9.9:47501, etc)</li>
+                        <li>IP address and port range (e.g. 127.0.0.1:47500..47510, 9.9.9.9:47501..47504, etc)</li>
+                        <li>Hostname (e.g. host1.com, host2, etc)</li>
+                        <li>Hostname and port (e.g. host1.com:47500, host2:47502, etc)</li>
+                        <li>Hostname and port range (e.g. host1.com:47500..47510, host2:47502..47508, etc)</li>
+                    </ul>
+                    If port is 0 or not provided then default port will be used (depends on discovery SPI configuration)<br />
+                    If port range is provided (e.g. host:port1..port2) the following should be considered:
+                    </ul>
+                    <ul>
+                        <li> port1 &lt; port2 should be true</li>
+                        <li> Both port1 and port2 should be greater than 0</li>
+                    </ul>`
+                })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/s3.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/s3.pug
new file mode 100644
index 0000000..dc18824
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/s3.pug
@@ -0,0 +1,55 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+mixin discovery-s3(modelAt = '$ctrl.clonedCluster')
+
+    -var discoveryKind = 'S3'
+    -var required = `${modelAt}.discovery.kind == '${discoveryKind}'`
+    -var model = `${modelAt}.discovery.S3`
+
+    .pc-form-grid-row&attributes(attributes=attributes)
+        .pc-form-grid-col-30
+            +form-field__text({
+                label: 'Bucket name:',
+                model: `${model}.bucketName`,
+                name: `'${discoveryKind}BucketName'`,
+                required: required,
+                placeholder: 'Input bucket name',
+                tip: 'Bucket name for IP finder'
+            })
+        .pc-form-grid-col-30
+            .pc-form-grid__text-only-item(style='font-style: italic;color: #424242;')
+                | AWS credentials will be generated as stub
+        .pc-form-grid-col-40(ng-if-start=`$ctrl.available("2.4.0")`)
+            +form-field__text({
+                label: 'Bucket endpoint:',
+                model: `${model}.bucketEndpoint`,
+                name: `'${discoveryKind}BucketEndpoint'`,
+                placeholder: 'Input bucket endpoint',
+                tip: 'Bucket endpoint for IP finder<br/> \
+                      For information about possible endpoint names visit <a href="http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region">docs.aws.amazon.com</a>'
+            })
+        .pc-form-grid-col-20(ng-if-end)
+            +form-field__text({
+                label: 'SSE algorithm:',
+                model: `${model}.SSEAlgorithm`,
+                name: `'${discoveryKind}SSEAlgorithm'`,
+                placeholder: 'Input SSE algorithm',
+                tip: 'Server-side encryption algorithm for Amazon S3-managed encryption keys<br/> \
+                      For information about possible S3-managed encryption keys visit <a href="http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html">docs.aws.amazon.com</a>'
+            })
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/shared.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/shared.pug
new file mode 100644
index 0000000..e5b86c3
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/shared.pug
@@ -0,0 +1,30 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+mixin discovery-shared(modelAt = '$ctrl.clonedCluster')
+    -const model = `${modelAt}.discovery.SharedFs`
+
+    .pc-form-grid-row&attributes(attributes=attributes)
+        .pc-form-grid-col-60
+            +form-field__text({
+                label: 'File path:',
+                model: `${model}.path`,
+                name: '"path"',
+                placeholder: 'disco/tcp',
+                tip: 'Shared path'
+            })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/vm.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/vm.pug
new file mode 100644
index 0000000..aee3a1b
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/vm.pug
@@ -0,0 +1,55 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+//- Static discovery
+mixin discovery-vm(modelAt = '$ctrl.clonedCluster')
+    -const model = `${modelAt}.discovery.Vm`
+    -const addresses = `${model}.addresses`
+
+    .pc-form-grid-row&attributes(attributes=attributes)
+        .pc-form-grid-col-60
+            .form-field.ignite-form-field
+                +list-addresses({
+                    items: addresses,
+                    name: 'vmAddresses',
+                    tip: `Addresses may be represented as follows:
+                        <ul>
+                            <li>IP address (e.g. 127.0.0.1, 9.9.9.9, etc)</li>
+                            <li>IP address and port (e.g. 127.0.0.1:47500, 9.9.9.9:47501, etc)</li>
+                            <li>IP address and port range (e.g. 127.0.0.1:47500..47510, 9.9.9.9:47501..47504, etc)</li>
+                            <li>Hostname (e.g. host1.com, host2, etc)</li>
+                            <li>Hostname and port (e.g. host1.com:47500, host2:47502, etc)</li>
+                            <li>Hostname and port range (e.g. host1.com:47500..47510, host2:47502..47508, etc)</li>
+                        </ul>
+                        If port is 0 or not provided then default port will be used (depends on discovery SPI configuration)<br />
+                        If port range is provided (e.g. host:port1..port2) the following should be considered:
+                        </ul>
+                        <ul>
+                            <li> port1 &lt; port2 should be true</li>
+                            <li> Both port1 and port2 should be greater than 0</li>
+                        </ul>`
+                })(
+                    ng-required='true'
+                    ng-ref='$vmAddresses'
+                    ng-ref-read='ngModel'
+                )
+                .form-field__errors(
+                    ng-messages=`$vmAddresses.$error`
+                    ng-show=`$vmAddresses.$invalid`
+                )
+                    +form-field__error({ error: 'required', message: 'Addresses should be configured' })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper.pug
new file mode 100644
index 0000000..53d704f
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper.pug
@@ -0,0 +1,115 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+mixin discovery-zookeeper(modelAt = '$ctrl.clonedCluster')
+
+    -var discoveryKind = 'ZooKeeper'
+    -var required = `${modelAt}.discovery.kind == '${discoveryKind}'`
+    -var model = `${modelAt}.discovery.ZooKeeper`
+    -var modelRetryPolicyKind = `${model}.retryPolicy.kind`
+
+    .pc-form-grid-row&attributes(attributes=attributes)
+        .pc-form-grid-col-60
+            +form-field__java-class({
+                label: 'Curator:',
+                model: `${model}.curator`,
+                name: '"curator"',
+                tip: 'The Curator framework in use<br/>\
+                     By default generates curator of org.apache.curator. framework.imps.CuratorFrameworkImpl\
+                     class with configured connect string, retry policy, and default session and connection timeouts',
+                validationActive: required
+            })
+        .pc-form-grid-col-60
+            +form-field__text({
+                label: 'Connect string:',
+                model: `${model}.zkConnectionString`,
+                name: `'${discoveryKind}ConnectionString'`,
+                required: required,
+                placeholder: 'host:port[chroot][,host:port[chroot]]',
+                tip: 'When <b>IGNITE_ZK_CONNECTION_STRING</b> system property is not configured this property will be used.<br><br>This should be a comma separated host:port pairs, each corresponding to a zk server. e.g. "127.0.0.1:3000,127.0.0.1:3001".<br>If the optional chroot suffix is used the example would look like: "127.0.0.1:3000,127.0.0.1:3002/app/a".<br><br>Where the client would be rooted at "/app/a" and all paths would be relative to this root - ie getting/setting/etc... "/foo/bar" would result in operations being run on "/app/a/foo/bar" (from the server perspective).<br><br><a href="https://zookeeper.apache.org/doc/r3.2.2/api/org/apache/zookeeper/ZooKeeper.html#ZooKeeper(java.lang.String,%20int,%20org.apache.zookeeper.Watcher)">Zookeeper docs</a>'
+            })
+        .pc-form-grid-col-60
+            +form-field__dropdown({
+                label: 'Retry policy:',
+                model: `${model}.retryPolicy.kind`,
+                name: '"retryPolicy"',
+                placeholder: 'Default',
+                options: '[\
+                                {value: "ExponentialBackoff", label: "Exponential backoff"},\
+                                {value: "BoundedExponentialBackoff", label: "Bounded exponential backoff"},\
+                                {value: "UntilElapsed", label: "Until elapsed"},\
+                                {value: "NTimes", label: "Max number of times"},\
+                                {value: "OneTime", label: "Only once"},\
+                                {value: "Forever", label: "Always allow retry"},\
+                                {value: "Custom", label: "Custom"},\
+                                {value: null, label: "Default"}\
+                            ]',
+                tip: 'Available retry policies:\
+                            <ul>\
+                                <li>Exponential backoff - retries a set number of times with increasing sleep time between retries</li>\
+                                <li>Bounded exponential backoff - retries a set number of times with an increasing (up to a maximum bound) sleep time between retries</li>\
+                                <li>Until elapsed - retries until a given amount of time elapses</li>\
+                                <li>Max number of times - retries a max number of times</li>\
+                                <li>Only once - retries only once</li>\
+                                <li>Always allow retry - retries infinitely</li>\
+                                <li>Custom - custom retry policy implementation</li>\
+                                <li>Default - exponential backoff retry policy with configured base sleep time equal to 1000ms and max retry count equal to 10</li>\
+                            </ul>'
+            })
+
+        .pc-form-grid__break
+
+        include ./zookeeper/retrypolicy/exponential-backoff
+        include ./zookeeper/retrypolicy/bounded-exponential-backoff
+        include ./zookeeper/retrypolicy/until-elapsed
+        include ./zookeeper/retrypolicy/n-times
+        include ./zookeeper/retrypolicy/one-time
+        include ./zookeeper/retrypolicy/forever
+        include ./zookeeper/retrypolicy/custom
+
+        .pc-form-grid-col-30
+            -var model = `${modelAt}.discovery.ZooKeeper`
+
+            +form-field__text({
+                label: 'Base path:',
+                model: `${model}.basePath`,
+                name: '"basePath"',
+                placeholder: '/services',
+                tip: 'Base path for service registration'
+            })
+        .pc-form-grid-col-30
+            +form-field__text({
+                label:'Service name:',
+                model: `${model}.serviceName`,
+                name: '"serviceName"',
+                placeholder: 'ignite',
+                tip: 'Service name to use, as defined by Curator&#39;s ServiceDiscovery recipe<br/>\
+                      In physical ZooKeeper terms, it represents the node under basePath, under which services will be registered'
+            })
+
+        .pc-form-grid__break
+
+        .pc-form-grid-col-60
+            +form-field__checkbox({
+                label: 'Allow duplicate registrations',
+                model: `${model}.allowDuplicateRegistrations`,
+                name: '"allowDuplicateRegistrations"',
+                tip: 'Whether to register each node only once, or if duplicate registrations are allowed<br/>\
+                     Nodes will attempt to register themselves, plus those they know about<br/>\
+                     By default, duplicate registrations are not allowed, but you might want to set this property to <b>true</b> if you have multiple network interfaces or if you are facing troubles'
+            })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/bounded-exponential-backoff.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/bounded-exponential-backoff.pug
new file mode 100644
index 0000000..84f7f2d
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/bounded-exponential-backoff.pug
@@ -0,0 +1,48 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var model = `${modelAt}.discovery.ZooKeeper.retryPolicy.BoundedExponentialBackoff`
+
+.pc-form-grid-col-20(ng-if-start=`${modelRetryPolicyKind} === 'BoundedExponentialBackoff'`)
+    +form-field__number({
+        label: 'Base interval:',
+        model: `${model}.baseSleepTimeMs`,
+        name: '"beBaseSleepTimeMs"',
+        placeholder: '1000',
+        min: '0',
+        tip: 'Initial amount of time in ms to wait between retries'
+    })
+.pc-form-grid-col-20
+    +form-field__number({
+        label: 'Max interval:',
+        model: `${model}.maxSleepTimeMs`,
+        name: '"beMaxSleepTimeMs"',
+        placeholder: 'Integer.MAX_VALUE',
+        min: '0',
+        tip: 'Max time in ms to sleep on each retry'
+    })
+.pc-form-grid-col-20(ng-if-end)
+    +form-field__number({
+        label: 'Max retries:',
+        model: `${model}.maxRetries`,
+        name: '"beMaxRetries"',
+        placeholder: '10',
+        min: '0',
+        max: '29',
+        tip: 'Max number of times to retry'
+    })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/custom.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/custom.pug
new file mode 100644
index 0000000..1cac6b8
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/custom.pug
@@ -0,0 +1,32 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var model = `${modelAt}.discovery.ZooKeeper.retryPolicy`
+-var retry = `${model}.Custom`
+-var required = `${modelAt}.discovery.kind === "ZooKeeper" && ${modelAt}.discovery.ZooKeeper.retryPolicy.kind === "Custom"`
+
+.pc-form-grid-col-60(ng-if-start=`${modelRetryPolicyKind} === 'Custom'`)
+    +form-field__java-class({
+        label: 'Class name:',
+        model: `${retry}.className`,
+        name: '"customClassName"',
+        required: required,
+        tip: 'Custom retry policy implementation class name',
+        validationActive: required
+    })
+.pc-form-grid__break(ng-if-end)
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/exponential-backoff.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/exponential-backoff.pug
new file mode 100644
index 0000000..ed0b5ff
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/exponential-backoff.pug
@@ -0,0 +1,48 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var model = `${modelAt}.discovery.ZooKeeper.retryPolicy.ExponentialBackoff`
+
+.pc-form-grid-col-20(ng-if-start=`${modelRetryPolicyKind} === 'ExponentialBackoff'`)
+    +form-field__number({
+        label: 'Base interval:',
+        model: `${model}.baseSleepTimeMs`,
+        name: '"expBaseSleepTimeMs"',
+        placeholder: '1000',
+        min: '0',
+        tip: 'Initial amount of time in ms to wait between retries'
+    })
+.pc-form-grid-col-20
+    +form-field__number({
+        label: 'Max retries:',
+        model: `${model}.maxRetries`,
+        name: '"expMaxRetries"',
+        placeholder: '10',
+        min: '0',
+        max: '29',
+        tip: 'Max number of times to retry'
+    })
+.pc-form-grid-col-20(ng-if-end)
+    +form-field__number({
+        label: 'Max interval:',
+        model: `${model}.maxSleepMs`,
+        name: '"expMaxSleepMs"',
+        placeholder: 'Integer.MAX_VALUE',
+        min: '0',
+        tip: 'Max time in ms to sleep on each retry'
+    })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/forever.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/forever.pug
new file mode 100644
index 0000000..e61a3c6
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/forever.pug
@@ -0,0 +1,30 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var model = `${modelAt}.discovery.ZooKeeper.retryPolicy.Forever`
+
+.pc-form-grid-col-30(ng-if-start=`${modelRetryPolicyKind} === 'Forever'`)
+    +form-field__number({
+        label: 'Interval:',
+        model: `${model}.retryIntervalMs`,
+        name: '"feRetryIntervalMs"',
+        placeholder: '1000',
+        min: '0',
+        tip: 'Time in ms between retry attempts'
+    })
+.pc-form-grid__break(ng-if-end)
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/n-times.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/n-times.pug
new file mode 100644
index 0000000..e44d030
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/n-times.pug
@@ -0,0 +1,38 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var model = `${modelAt}.discovery.ZooKeeper.retryPolicy.NTimes`
+
+.pc-form-grid-col-30(ng-if-start=`${modelRetryPolicyKind} === 'NTimes'`)
+    +form-field__number({
+        label: 'Retries:',
+        model: `${model}.n`,
+        name: '"n"',
+        placeholder: '10',
+        min: '0',
+        tip: 'Number of times to retry'
+    })
+.pc-form-grid-col-30(ng-if-end)
+    +form-field__number({
+        label: 'Interval:',
+        model: `${model}.sleepMsBetweenRetries`,
+        name: '"ntSleepMsBetweenRetries"',
+        placeholder: '1000',
+        min: '0',
+        tip: 'Time in ms between retry attempts'
+    })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/one-time.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/one-time.pug
new file mode 100644
index 0000000..4d86f5c
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/one-time.pug
@@ -0,0 +1,30 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var model = `${modelAt}.discovery.ZooKeeper.retryPolicy.OneTime`
+
+.pc-form-grid-col-30(ng-if-start=`${modelRetryPolicyKind} === 'OneTime'`)
+    +form-field__number({
+        label: 'Interval:',
+        model: `${model}.sleepMsBetweenRetry`,
+        name: '"oneSleepMsBetweenRetry"',
+        placeholder: '1000',
+        min: '0',
+        tip: 'Time in ms to retry attempt'
+    })
+.pc-form-grid__break(ng-if-end)
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/until-elapsed.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/until-elapsed.pug
new file mode 100644
index 0000000..acb1dff
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper/retrypolicy/until-elapsed.pug
@@ -0,0 +1,38 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var model = `${modelAt}.discovery.ZooKeeper.retryPolicy.UntilElapsed`
+
+.pc-form-grid-col-30(ng-if-start=`${modelRetryPolicyKind} === 'UntilElapsed'`)
+    +form-field__number({
+        label: 'Total time:',
+        model: `${model}.maxElapsedTimeMs`,
+        name: '"ueMaxElapsedTimeMs"',
+        placeholder: '60000',
+        min: '0',
+        tip: 'Total time in ms for execution of retry attempt'
+    })
+.pc-form-grid-col-30(ng-if-end)
+    +form-field__number({
+        label: 'Interval:',
+        model: `${model}.sleepMsBetweenRetries`,
+        name: '"ueSleepMsBetweenRetries"',
+        placeholder: '1000',
+        min: '0',
+        tip: 'Time in ms between retry attempts'
+    })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/hadoop.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/hadoop.pug
new file mode 100644
index 0000000..58bb21c
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/hadoop.pug
@@ -0,0 +1,147 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'hadoop'
+-var model = '$ctrl.clonedCluster.hadoopConfiguration'
+-var plannerModel = model + '.mapReducePlanner'
+-var weightedModel = plannerModel + '.Weighted'
+-var weightedPlanner = plannerModel + '.kind === "Weighted"'
+-var customPlanner = plannerModel + '.kind === "Custom"'
+-var libs = model + '.nativeLibraryNames'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Hadoop configuration
+    panel-description Hadoop Accelerator configuration.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'Map reduce planner:',
+                    model: `${plannerModel}.kind`,
+                    name: '"MapReducePlanner"',
+                    placeholder: 'Default',
+                    options: '[\
+                        {value: "Weighted", label: "Weighted"},\
+                        {value: "Custom", label: "Custom"},\
+                        {value: null, label: "Default"}\
+                    ]',
+                    tip: 'Implementation of map reduce planner\
+                        <ul>\
+                            <li>Weighted - Planner which assigns mappers and reducers based on their "weights"</li>\
+                            <li>Custom - Custom planner implementation</li>\
+                            <li>Default - Default planner implementation</li>\
+                        </ul>'
+                })
+            .pc-form-group.pc-form-grid-row(ng-show=weightedPlanner)
+                .pc-form-grid-col-20
+                    +form-field__number({
+                        label: 'Local mapper weight:',
+                        model: `${weightedModel}.localMapperWeight`,
+                        name: '"LocalMapperWeight"',
+                        placeholder: '100',
+                        min: '0',
+                        tip: 'This weight is added to a node when a mapper is assigned and it is input split data is located on this node'
+                    })
+                .pc-form-grid-col-20
+                    +form-field__number({
+                        label: 'Remote mapper weight:',
+                        model: `${weightedModel}.remoteMapperWeight`,
+                        name: '"remoteMapperWeight"',
+                        placeholder: '100',
+                        min: '0',
+                        tip: 'This weight is added to a node when a mapper is assigned, but it is input split data is not located on this node'
+                    })
+                .pc-form-grid-col-20
+                    +form-field__number({
+                        label: 'Local reducer weight:',
+                        model: `${weightedModel}.localReducerWeight`,
+                        name: '"localReducerWeight"',
+                        placeholder: '100',
+                        min: '0',
+                        tip: 'This weight is added to a node when a reducer is assigned and the node have at least one assigned mapper'
+                    })
+                .pc-form-grid-col-30
+                    +form-field__number({
+                        label: 'Remote reducer weight:',
+                        model: `${weightedModel}.remoteReducerWeight`,
+                        name: '"remoteReducerWeight"',
+                        placeholder: '100',
+                        min: '0',
+                        tip: 'This weight is added to a node when a reducer is assigned, but the node does not have any assigned mappers'
+                    })
+                .pc-form-grid-col-30
+                    +form-field__number({
+                        label: 'Local mapper weight:',
+                        model: `${weightedModel}.preferLocalReducerThresholdWeight`,
+                        name: '"preferLocalReducerThresholdWeight"',
+                        placeholder: '200',
+                        min: '0',
+                        tip: 'When threshold is reached, a node with mappers is no longer considered as preferred for further reducer assignments'
+                    })
+            .pc-form-group.pc-form-grid-row(ng-show=customPlanner)
+                .pc-form-grid-col-60
+                    +form-field__java-class({
+                        label: 'Class name:',
+                        model: `${plannerModel}.Custom.className`,
+                        name: '"MapReducePlannerCustomClass"',
+                        required: customPlanner,
+                        tip: 'Custom planner implementation'
+                    })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Finished job info TTL:',
+                    model: `${model}.finishedJobInfoTtl`,
+                    name: '"finishedJobInfoTtl"',
+                    placeholder: '30000',
+                    min: '0',
+                    tip: 'Finished job info time-to-live in milliseconds'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Max parallel tasks:',
+                    model: `${model}.maxParallelTasks`,
+                    name: '"maxParallelTasks"',
+                    placeholder: 'availableProcessors * 2',
+                    min: '1',
+                    tip: 'Max number of local tasks that may be executed in parallel'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Max task queue size:',
+                    model: `${model}.maxTaskQueueSize`,
+                    name: '"maxTaskQueueSize"',
+                    placeholder: '8192',
+                    min: '1',
+                    tip: 'Max task queue size'
+                })
+            .pc-form-grid-col-60
+                .ignite-form-field
+                    +list-text-field({
+                        items: libs,
+                        lbl: 'Library name',
+                        name: 'libraryName',
+                        itemName: 'library name',
+                        itemsName: 'library names'
+                    })(
+                        list-editable-cols=`::[{name: 'Native libraries:'}]`
+                    )
+                        +form-field__error({ error: 'igniteUnique', message: 'Such native library already exists!' })
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterHadoop')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/load-balancing.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/load-balancing.pug
new file mode 100644
index 0000000..240a2dd
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/load-balancing.pug
@@ -0,0 +1,183 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var model = '$ctrl.clonedCluster'
+-var form = 'loadBalancing'
+-var loadBalancingSpi = model + '.loadBalancingSpi'
+-var loadBalancingCustom = '$item.kind === "Custom"'
+-var loadProbeCustom = '$item.kind === "Adaptive" && $item.Adaptive.loadProbe.kind === "Custom"'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Load balancing configuration
+    panel-description
+        | Load balancing component balances job distribution among cluster nodes.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/load-balancing" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6
+            mixin clusters-load-balancing-spi
+                .ignite-form-field(ng-init='loadBalancingSpiTbl={type: "loadBalancingSpi", model: "loadBalancingSpi", focusId: "kind", ui: "load-balancing-table"}')
+                    +form-field__label({ label: 'Load balancing configurations:', name: '"loadBalancingConfigurations"' })
+                        +form-field__tooltip(`Load balancing component balances job distribution among cluster nodes`)
+
+                    -let items = loadBalancingSpi
+                    list-editable.pc-list-editable-with-legacy-settings-rows(
+                        ng-model=items
+                        name='loadBalancingConfigurations'
+                    )
+                        list-editable-item-edit
+                            - form = '$parent.form'
+                            .settings-row
+                                +form-field__dropdown({
+                                    label: 'Load balancing:',
+                                    model: '$item.kind',
+                                    name: '"loadBalancingKind"',
+                                    required: true,
+                                    options: '::$ctrl.Clusters.loadBalancingKinds',
+                                    tip: `Provides the next best balanced node for job execution
+                                    <ul>
+                                        <li>Round-robin - Iterates through nodes in round-robin fashion and pick the next sequential node</li>
+                                        <li>Adaptive - Adapts to overall node performance</li>
+                                        <li>Random - Picks a random node for job execution</li>
+                                        <li>Custom - Custom load balancing implementation</li>
+                                    </ul>`
+                                })(
+                                    ignite-unique=`${loadBalancingSpi}`
+                                    ignite-unique-property='kind'
+                                )
+                                    +form-field__error({ error: 'igniteUnique', message: 'Load balancing SPI of that type is already configured' })
+                            .settings-row(ng-show='$item.kind === "RoundRobin"')
+                                +form-field__checkbox({
+                                    label: 'Per task',
+                                    model: '$item.RoundRobin.perTask',
+                                    name: '"loadBalancingRRPerTask"',
+                                    tip: 'A new round robin order should be created for every task flag'
+                                })
+                            .settings-row(ng-show='$item.kind === "Adaptive"')
+                                +form-field__dropdown({
+                                    label: 'Load probe:',
+                                    model: '$item.Adaptive.loadProbe.kind',
+                                    name: '"loadBalancingAdaptiveLoadProbeKind"',
+                                    placeholder: 'Default',
+                                    options: '[\
+                                        {value: "Job", label: "Job count"},\
+                                        {value: "CPU", label: "CPU load"},\
+                                        {value: "ProcessingTime", label: "Processing time"},\
+                                        {value: "Custom", label: "Custom"},\
+                                        {value: null, label: "Default"}\
+                                    ]',
+                                    tip: 'Implementation of node load probing\
+                                        <ul>\
+                                            <li>Job count - Based on active and waiting job count</li>\
+                                            <li>CPU load - Based on CPU load</li>\
+                                            <li>Processing time - Based on total job processing time</li>\
+                                            <li>Custom - Custom load probing implementation</li>\
+                                            <li>Default - Default load probing implementation</li>\
+                                        </ul>'
+                                })
+                            .settings-row(ng-show='$item.kind === "Adaptive" && $item.Adaptive.loadProbe.kind')
+                                .panel-details(ng-show='$item.Adaptive.loadProbe.kind === "Job"')
+                                    .details-row
+                                        +form-field__checkbox({
+                                            label: 'Use average',
+                                            model: '$item.Adaptive.loadProbe.Job.useAverage',
+                                            name: '"loadBalancingAdaptiveJobUseAverage"',
+                                            tip: 'Use average CPU load vs. current'
+                                        })
+                                .panel-details(ng-show='$item.Adaptive.loadProbe.kind === "CPU"')
+                                    .details-row
+                                        +form-field__checkbox({
+                                            label: 'Use average',
+                                            model: '$item.Adaptive.loadProbe.CPU.useAverage',
+                                            name: '"loadBalancingAdaptiveCPUUseAverage"',
+                                            tip: 'Use average CPU load vs. current'
+                                        })
+                                    .details-row
+                                        +form-field__checkbox({
+                                            label: 'Use processors',
+                                            model: '$item.Adaptive.loadProbe.CPU.useProcessors',
+                                            name: '"loadBalancingAdaptiveCPUUseProcessors"',
+                                            tip: 'Divide each node\'s CPU load by the number of processors on that node'
+                                        })
+                                    .details-row
+                                        +form-field__number({
+                                            label: 'Processor coefficient:',
+                                            model: '$item.Adaptive.loadProbe.CPU.processorCoefficient',
+                                            name: '"loadBalancingAdaptiveCPUProcessorCoefficient"',
+                                            placeholder: '1',
+                                            min: '0.001',
+                                            max: '1',
+                                            step: '0.05',
+                                            tip: 'Coefficient of every CPU'
+                                        })
+                                .panel-details(ng-show='$item.Adaptive.loadProbe.kind === "ProcessingTime"')
+                                    .details-row
+                                        +form-field__checkbox({
+                                            label: 'Use average',
+                                            model: '$item.Adaptive.loadProbe.ProcessingTime.useAverage',
+                                            name: '"loadBalancingAdaptiveJobUseAverage"',
+                                            tip: 'Use average execution time vs. current'
+                                        })
+                                .panel-details(ng-show=loadProbeCustom)
+                                    .details-row
+                                        +form-field__java-class({
+                                            label: 'Load brobe implementation:',
+                                            model: '$item.Adaptive.loadProbe.Custom.className',
+                                            name: '"loadBalancingAdaptiveJobUseClass"',
+                                            required: loadProbeCustom,
+                                            tip: 'Custom load balancing SPI implementation class name.',
+                                            validationActive: loadProbeCustom
+                                        })
+
+                            .settings-row(ng-show='$item.kind === "WeightedRandom"')
+                                +form-field__number({
+                                    label: 'Node weight:',
+                                    model: '$item.WeightedRandom.nodeWeight',
+                                    name: '"loadBalancingWRNodeWeight"',
+                                    placeholder: '10',
+                                    min: '1',
+                                    tip: 'Weight of node'
+                                })
+                            .settings-row(ng-show='$item.kind === "WeightedRandom"')
+                                +form-field__checkbox({
+                                    label: 'Use weights',
+                                    model: '$item.WeightedRandom.useWeights',
+                                    name: '"loadBalancingWRUseWeights"',
+                                    tip: 'Node weights should be checked when doing random load balancing'
+                                })
+                            .settings-row(ng-show=loadBalancingCustom)
+                                +form-field__java-class({
+                                    label: 'Load balancing SPI implementation:',
+                                    model: '$item.Custom.className',
+                                    name: '"loadBalancingClass"',
+                                    required: loadBalancingCustom,
+                                    tip: 'Custom load balancing SPI implementation class name.',
+                                    validationActive: loadBalancingCustom
+                                })
+
+                        list-editable-no-items
+                            list-editable-add-item-button(
+                                add-item=`$ctrl.Clusters.addLoadBalancingSpi(${model})`
+                                label-single='load balancing configuration'
+                                label-multiple='load balancing configurations'
+                            )
+
+            +clusters-load-balancing-spi
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterLoadBalancing')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/logger.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/logger.pug
new file mode 100644
index 0000000..231f50c
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/logger.pug
@@ -0,0 +1,66 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'logger'
+-var model = '$ctrl.clonedCluster.logger'
+-var kind = model + '.kind'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Logger configuration
+    panel-description Logging functionality used throughout the system.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'Logger:',
+                    model: kind,
+                    name: '"logger"',
+                    placeholder: 'Default',
+                    options: '[\
+                        {value: "Log4j", label: "Apache Log4j"},\
+                        {value: "Log4j2", label: "Apache Log4j 2"},\
+                        {value: "SLF4J", label: "Simple Logging Facade (SLF4J)"},\
+                        {value: "Java", label: "Java logger (JUL)"},\
+                        {value: "JCL", label: "Jakarta Commons Logging (JCL)"},\
+                        {value: "Null", label: "Null logger"},\
+                        {value: "Custom", label: "Custom"},\
+                        {value: null, label: "Default"}\
+                    ]',
+                    tip: 'Logger implementations\
+                       <ul>\
+                           <li>Apache Log4j - log4j-based logger</li>\
+                           <li>Apache Log4j 2 - Log4j2-based logger</li>\
+                           <li>Simple Logging Facade (SLF4J) - SLF4j-based logger</li>\
+                           <li>Java logger (JUL) - built in java logger</li>\
+                           <li>Jakarta Commons Logging (JCL) - wraps any JCL (Jakarta Commons Logging) loggers</li>\
+                           <li>Null logger - logger which does not output anything</li>\
+                           <li>Custom - custom logger implementation</li>\
+                           <li>Default - Apache Log4j if awailable on classpath or Java logger otherwise</li>\
+                       </ul>'
+                })
+            .pc-form-group(ng-show=`${kind} && (${kind} === 'Log4j2' || ${kind} === 'Log4j' || ${kind} === 'Custom')`)
+                .pc-form-grid-row(ng-show=`${kind} === 'Log4j2'`)
+                    include ./logger/log4j2
+                .pc-form-grid-row(ng-show=`${kind} === 'Log4j'`)
+                    include ./logger/log4j
+                .pc-form-grid-row(ng-show=`${kind} === 'Custom'`)
+                    include ./logger/custom
+        .pca-form-column-6
+            -var model = '$ctrl.clonedCluster.logger'
+            +preview-xml-java(model, 'clusterLogger')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/logger/custom.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/logger/custom.pug
new file mode 100644
index 0000000..589a9c0
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/logger/custom.pug
@@ -0,0 +1,31 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var form = 'logger'
+-var model = '$ctrl.clonedCluster.logger.Custom'
+-var required = '$ctrl.clonedCluster.logger.kind === "Custom"'
+
+.pc-form-grid-col-60
+    +form-field__java-class({
+        label: 'Class:',
+        model: `${model}.class`,
+        name: '"customLogger"',
+        required: required,
+        tip: 'Logger implementation class name',
+        validationActive: required
+    })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/logger/log4j.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/logger/log4j.pug
new file mode 100644
index 0000000..1f216b7
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/logger/log4j.pug
@@ -0,0 +1,68 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var form = 'logger'
+-var model = '$ctrl.clonedCluster.logger.Log4j'
+-var pathRequired = model + '.mode === "Path" && $ctrl.clonedCluster.logger.kind === "Log4j"'
+
+.pc-form-grid-col-30
+    +form-field__dropdown({
+        label: 'Level:',
+        model: `${model}.level`,
+        name: '"log4jLevel"',
+        placeholder: 'Default',
+        options: '[\
+                    {value: "OFF", label: "OFF"},\
+                    {value: "FATAL", label: "FATAL"},\
+                    {value: "ERROR", label: "ERROR"},\
+                    {value: "WARN", label: "WARN"},\
+                    {value: "INFO", label: "INFO"},\
+                    {value: "DEBUG", label: "DEBUG"},\
+                    {value: "TRACE", label: "TRACE"},\
+                    {value: "ALL", label: "ALL"},\
+                    {value: null, label: "Default"}\
+                ]',
+        tip: 'Level for internal log4j implementation'
+    })
+
+.pc-form-grid-col-30
+    +form-field__dropdown({
+        label: 'Logger configuration:',
+        model: `${model}.mode`,
+        name: '"log4jMode"',
+        required: 'true',
+        placeholder: 'Choose logger mode',
+        options: '[\
+                    {value: "Default", label: "Default"},\
+                    {value: "Path", label: "Path"}\
+                ]',
+        tip: 'Choose logger configuration\
+                <ul>\
+                    <li>Default - default logger</li>\
+                    <li>Path - path or URI to XML configuration</li>\
+                </ul>'
+    })
+.pc-form-grid-col-60(ng-show=pathRequired)
+    +form-field__text({
+        label: 'Path:',
+        model: `${model}.path`,
+        name: '"log4jPath"',
+        required: pathRequired,
+        placeholder: 'Input path',
+        tip: 'Path or URI to XML configuration'
+    })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/logger/log4j2.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/logger/log4j2.pug
new file mode 100644
index 0000000..c5b785b
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/logger/log4j2.pug
@@ -0,0 +1,50 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+-var form = 'logger'
+-var model = '$ctrl.clonedCluster.logger.Log4j2'
+-var log4j2Required = '$ctrl.clonedCluster.logger.kind === "Log4j2"'
+
+.pc-form-grid-col-60
+    +form-field__dropdown({
+        label: 'Level:',
+        model: `${model}.level`,
+        name: '"log4j2Level"',
+        placeholder: 'Default',
+        options: '[\
+                    {value: "OFF", label: "OFF"},\
+                    {value: "FATAL", label: "FATAL"},\
+                    {value: "ERROR", label: "ERROR"},\
+                    {value: "WARN", label: "WARN"},\
+                    {value: "INFO", label: "INFO"},\
+                    {value: "DEBUG", label: "DEBUG"},\
+                    {value: "TRACE", label: "TRACE"},\
+                    {value: "ALL", label: "ALL"},\
+                    {value: null, label: "Default"}\
+                ]',
+        tip: 'Level for internal log4j2 implementation'
+    })
+.pc-form-grid-col-60
+    +form-field__text({
+        label: 'Path:',
+        model: `${model}.path`,
+        name: '"log4j2Path"',
+        required: log4j2Required,
+        placeholder: 'Input path',
+        tip: 'Path or URI to XML configuration'
+    })
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/marshaller.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/marshaller.pug
new file mode 100644
index 0000000..da2effd
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/marshaller.pug
@@ -0,0 +1,115 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'marshaller'
+-var model = '$ctrl.clonedCluster'
+-var marshaller = model + '.marshaller'
+-var optMarshaller = marshaller + '.OptimizedMarshaller'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Marshaller
+    panel-description
+        | Marshaller allows to marshal or unmarshal objects in grid.
+        | It provides serialization/deserialization mechanism for all instances that are sent across networks or are otherwise serialized.
+        | By default BinaryMarshaller will be used.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/binary-marshaller" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60(ng-if='$ctrl.available(["1.0.0", "2.0.0"])')
+                +form-field__dropdown({
+                    label: 'Marshaller:',
+                    model: marshaller + '.kind',
+                    name: '"kind"',
+                    placeholder: 'Default',
+                    options: '$ctrl.marshallerVariant',
+                    tip: 'Instance of marshaller to use in grid<br/>\
+                       <ul>\
+                           <li>OptimizedMarshaller - Optimized implementation of marshaller</li>\
+                           <li>JdkMarshaller - Marshaller based on JDK serialization mechanism</li>\
+                           <li>Default - BinaryMarshaller serialize and deserialize all objects in the binary format</li>\
+                       </ul>'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available(["2.0.0", "2.1.0"])')
+                +form-field__dropdown({
+                    label: 'Marshaller:',
+                    model: marshaller + '.kind',
+                    name: '"kind"',
+                    placeholder: 'Default',
+                    options: '$ctrl.marshallerVariant',
+                    tip: 'Instance of marshaller to use in grid<br/>\
+                        <ul>\
+                            <li>JdkMarshaller - Marshaller based on JDK serialization mechanism</li>\
+                            <li>Default - BinaryMarshaller serialize and deserialize all objects in the binary format</li>\
+                        </ul>'
+                })
+            .pc-form-group.pc-form-grid-row(
+                ng-show=`${marshaller}.kind === 'OptimizedMarshaller'`
+                ng-if='$ctrl.available(["1.0.0", "2.1.0"])'
+            )
+                .pc-form-grid-col-60
+                    +form-field__number({
+                        label: 'Streams pool size:',
+                        model: `${optMarshaller}.poolSize`,
+                        name: '"poolSize"',
+                        placeholder: '0',
+                        min: '0',
+                        tip: 'Specifies size of cached object streams used by marshaller<br/>\
+                             Object streams are cached for performance reason to avoid costly recreation for every serialization routine<br/>\
+                             If 0 (default), pool is not used and each thread has its own cached object stream which it keeps reusing<br/>\
+                             Since each stream has an internal buffer, creating a stream for each thread can lead to high memory consumption if many large messages are marshalled or unmarshalled concurrently<br/>\
+                             Consider using pool in this case. This will limit number of streams that can be created and, therefore, decrease memory consumption<br/>\
+                             NOTE: Using streams pool can decrease performance since streams will be shared between different threads which will lead to more frequent context switching'
+                    })
+                .pc-form-grid-col-60
+                    +form-field__checkbox({
+                        label: 'Require serializable',
+                        model: `${optMarshaller}.requireSerializable`,
+                        name: '"requireSerializable"',
+                        tip: 'Whether marshaller should require Serializable interface or not'
+                    })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Marshal local jobs',
+                    model: `${model}.marshalLocalJobs`,
+                    name: '"marshalLocalJobs"',
+                    tip: 'If this flag is enabled, jobs mapped to local node will be marshalled as if it was remote node'
+                })
+
+            //- Removed in ignite 2.0
+            .pc-form-grid-col-30(ng-if-start='$ctrl.available(["1.0.0", "2.0.0"])')
+                +form-field__number({
+                    label: 'Keep alive time:',
+                    model: `${model}.marshallerCacheKeepAliveTime`,
+                    name: '"marshallerCacheKeepAliveTime"',
+                    placeholder: '10000',
+                    min: '0',
+                    tip: 'Keep alive time of thread pool that is in charge of processing marshaller messages'
+                })
+            .pc-form-grid-col-30(ng-if-end)
+                +form-field__number({
+                    label: 'Pool size:',
+                    model: `${model}.marshallerCacheThreadPoolSize`,
+                    name: '"marshallerCacheThreadPoolSize"',
+                    placeholder: 'max(8, availableProcessors) * 2',
+                    min: '1',
+                    tip: 'Default size of thread pool that is in charge of processing marshaller messages'
+                })
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterMarshaller')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/memory.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/memory.pug
new file mode 100644
index 0000000..9fa53f5
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/memory.pug
@@ -0,0 +1,240 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'memoryConfiguration'
+-var model = '$ctrl.clonedCluster.memoryConfiguration'
+-var memoryPolicies = model + '.memoryPolicies'
+
+panel-collapsible(
+ng-form=form
+on-open=`ui.loadPanel('${form}')`
+ng-show='$ctrl.available(["2.0.0", "2.3.0"])'
+)
+    panel-title Memory configuration
+    panel-description
+        | Page memory is a manageable off-heap based memory architecture that is split into pages of fixed size.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/durable-memory" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`$ctrl.available(["2.0.0", "2.3.0"]) && ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'Page size:',
+                    model: `${model}.pageSize`,
+                    name: '"MemoryConfigurationPageSize"',
+                    options: `$ctrl.Clusters.memoryConfiguration.pageSize.values`,
+                    tip: 'Every memory region is split on pages of fixed size'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Concurrency level:',
+                    model: `${model}.concurrencyLevel`,
+                    name: '"MemoryConfigurationConcurrencyLevel"',
+                    placeholder: 'availableProcessors',
+                    min: '2',
+                    tip: 'The number of concurrent segments in Ignite internal page mapping tables'
+                })
+            .pc-form-grid-col-60.pc-form-group__text-title
+                span System cache
+            .pc-form-group.pc-form-grid-row
+                .pc-form-grid-col-30
+                    form-field-size(
+                        label='Initial size:'
+                        ng-model=`${model}.systemCacheInitialSize`
+                        name='systemCacheInitialSize'
+                        placeholder='{{ $ctrl.Clusters.memoryConfiguration.systemCacheInitialSize.default / systemCacheInitialSizeScale.value }}'
+                        min='{{ ::$ctrl.Clusters.memoryConfiguration.systemCacheInitialSize.min }}'
+                        tip='Initial size of a memory region reserved for system cache'
+                        on-scale-change='systemCacheInitialSizeScale = $event'
+                    )
+                .pc-form-grid-col-30
+                    form-field-size(
+                        label='Max size:'
+                        ng-model=`${model}.systemCacheMaxSize`
+                        name='systemCacheMaxSize'
+                        placeholder='{{ $ctrl.Clusters.memoryConfiguration.systemCacheMaxSize.default / systemCacheMaxSizeScale.value }}'
+                        min='{{ $ctrl.Clusters.memoryConfiguration.systemCacheMaxSize.min($ctrl.clonedCluster) }}'
+                        tip='Maximum size of a memory region reserved for system cache'
+                        on-scale-change='systemCacheMaxSizeScale = $event'
+                    )
+            .pc-form-grid-col-60.pc-form-group__text-title
+                span Memory policies
+            .pc-form-group.pc-form-grid-row
+                .pc-form-grid-col-60
+                    +form-field__text({
+                        label: 'Default memory policy name:',
+                        model: `${model}.defaultMemoryPolicyName`,
+                        name: '"defaultMemoryPolicyName"',
+                        placeholder: '{{ ::$ctrl.Clusters.memoryPolicy.name.default }}',
+                        tip: 'Name of a memory policy to be used as default one'
+                    })(
+                        pc-not-in-collection='::$ctrl.Clusters.memoryPolicy.name.invalidValues'
+                        ui-validate=`{
+                                defaultMemoryPolicyExists: '$ctrl.Clusters.memoryPolicy.customValidators.defaultMemoryPolicyExists($value, ${memoryPolicies})'
+                            }`
+                        ui-validate-watch=`"${memoryPolicies}"`
+                        ui-validate-watch-object-equality='true'
+                        ng-model-options='{allowInvalid: true}'
+                    )
+                        +form-field__error({ error: 'notInCollection', message: '{{::$ctrl.Clusters.memoryPolicy.name.invalidValues[0]}} is reserved for internal use' })
+                        +form-field__error({ error: 'defaultMemoryPolicyExists', message: 'Memory policy with that name should be configured' })
+                .pc-form-grid-col-60(ng-hide='(' + model + '.defaultMemoryPolicyName || "default") !== "default"')
+                    +form-field__number({
+                        label: 'Default memory policy size:',
+                        model: `${model}.defaultMemoryPolicySize`,
+                        name: '"defaultMemoryPolicySize"',
+                        placeholder: '0.8 * totalMemoryAvailable',
+                        min: '10485760',
+                        tip: 'Specify desired size of default memory policy without having to use more verbose syntax of MemoryPolicyConfiguration elements'
+                    })
+                .pc-form-grid-col-60
+                    mixin clusters-memory-policies
+                        .ignite-form-field(ng-init='memoryPoliciesTbl={type: "memoryPolicies", model: "memoryPolicies", focusId: "name", ui: "memory-policies-table"}')
+                            +form-field__label({ label: 'Configured policies:', name: '"configuredPolicies"' })
+                                +form-field__tooltip({ title: `List of configured policies` })
+
+                            -let items = memoryPolicies
+                            list-editable.pc-list-editable-with-form-grid(ng-model=items name='memoryPolicies')
+                                list-editable-item-edit.pc-form-grid-row
+                                    - form = '$parent.form'
+                                    .pc-form-grid-col-60
+                                        +form-field__text({
+                                            label: 'Name:',
+                                            model: '$item.name',
+                                            name: '"MemoryPolicyName"',
+                                            placeholder: '{{ ::$ctrl.Clusters.memoryPolicy.name.default }}',
+                                            tip: 'Memory policy name'
+                                        })(
+                                            ui-validate=`{
+                                                    uniqueMemoryPolicyName: '$ctrl.Clusters.memoryPolicy.customValidators.uniqueMemoryPolicyName($item, ${items})'
+                                                }`
+                                            ui-validate-watch=`"${items}"`
+                                            ui-validate-watch-object-equality='true'
+                                            pc-not-in-collection='::$ctrl.Clusters.memoryPolicy.name.invalidValues'
+                                            ng-model-options='{allowInvalid: true}'
+                                        )
+                                            +form-field__error({ error: 'uniqueMemoryPolicyName', message: 'Memory policy with that name is already configured' })
+                                            +form-field__error({ error: 'notInCollection', message: '{{::$ctrl.Clusters.memoryPolicy.name.invalidValues[0]}} is reserved for internal use' })
+                                    .pc-form-grid-col-60
+                                        form-field-size(
+                                            label='Initial size:'
+                                            ng-model='$item.initialSize'
+                                            ng-model-options='{allowInvalid: true}'
+                                            name='MemoryPolicyInitialSize'
+                                            placeholder='{{ $ctrl.Clusters.memoryPolicy.initialSize.default / scale.value }}'
+                                            min='{{ ::$ctrl.Clusters.memoryPolicy.initialSize.min }}'
+                                            tip='Initial memory region size defined by this memory policy'
+                                            on-scale-change='scale = $event'
+                                        )
+                                    .pc-form-grid-col-60
+                                        form-field-size(
+                                            ng-model='$item.maxSize'
+                                            ng-model-options='{allowInvalid: true}'
+                                            name='MemoryPolicyMaxSize'
+                                            label='Maximum size:'
+                                            placeholder='{{ ::$ctrl.Clusters.memoryPolicy.maxSize.default }}'
+                                            min='{{ $ctrl.Clusters.memoryPolicy.maxSize.min($item) }}'
+                                            tip='Maximum memory region size defined by this memory policy'
+                                        )
+                                    .pc-form-grid-col-60
+                                        +form-field__text({
+                                            label: 'Swap file path:',
+                                            model: '$item.swapFilePath',
+                                            name: '"MemoryPolicySwapFilePath"',
+                                            placeholder: 'Input swap file path',
+                                            tip: 'An optional path to a memory mapped file for this memory policy'
+                                        })
+                                    .pc-form-grid-col-60
+                                        +form-field__dropdown({
+                                            label: 'Eviction mode:',
+                                            model: '$item.pageEvictionMode',
+                                            name: '"MemoryPolicyPageEvictionMode"',
+                                            placeholder: 'DISABLED',
+                                            options: '[\
+                                                {value: "DISABLED", label: "DISABLED"},\
+                                                {value: "RANDOM_LRU", label: "RANDOM_LRU"},\
+                                                {value: "RANDOM_2_LRU", label: "RANDOM_2_LRU"}\
+                                            ]',
+                                            tip: 'An algorithm for memory pages eviction\
+                                                 <ul>\
+                                                    <li>DISABLED - Eviction is disabled</li>\
+                                                    <li>RANDOM_LRU - Once a memory region defined by a memory policy is configured, an off - heap array is allocated to track last usage timestamp for every individual data page</li>\
+                                                    <li>RANDOM_2_LRU - Differs from Random - LRU only in a way that two latest access timestamps are stored for every data page</li>\
+                                                 </ul>'
+                                        })
+                                    .pc-form-grid-col-30
+                                        +form-field__number({
+                                            label: 'Eviction threshold:',
+                                            model: '$item.evictionThreshold',
+                                            name: '"MemoryPolicyEvictionThreshold"',
+                                            placeholder: '0.9',
+                                            min: '0.5',
+                                            max: '0.999',
+                                            step: '0.05',
+                                            tip: 'A threshold for memory pages eviction initiation'
+                                        })
+                                    .pc-form-grid-col-30
+                                        +form-field__number({
+                                            label: 'Empty pages pool size:',
+                                            model: '$item.emptyPagesPoolSize',
+                                            name: '"MemoryPolicyEmptyPagesPoolSize"',
+                                            placeholder: '{{ ::$ctrl.Clusters.memoryPolicy.emptyPagesPoolSize.default }}',
+                                            min: '{{ ::$ctrl.Clusters.memoryPolicy.emptyPagesPoolSize.min }}',
+                                            max: '{{ $ctrl.Clusters.memoryPolicy.emptyPagesPoolSize.max($ctrl.clonedCluster, $item) }}',
+                                            tip: 'The minimal number of empty pages to be present in reuse lists for this memory policy'
+                                        })
+
+                                    //- Since ignite 2.1
+                                    .pc-form-grid-col-30(ng-if-start='$ctrl.available("2.1.0")')
+                                        +form-field__number({
+                                            label: 'Sub intervals:',
+                                            model: '$item.subIntervals',
+                                            name: '"MemoryPolicySubIntervals"',
+                                            placeholder: '5',
+                                            min: '1',
+                                            tip: 'A number of sub-intervals the whole rate time interval will be split into to calculate allocation and eviction rates'
+                                        })
+                                    .pc-form-grid-col-30(ng-if-end)
+                                        +form-field__number({
+                                            label: 'Rate time interval:',
+                                            model: '$item.rateTimeInterval',
+                                            name: '"MemoryPolicyRateTimeInterval"',
+                                            placeholder: '60000',
+                                            min: '1000',
+                                            tip: 'Time interval for allocation rate and eviction rate monitoring purposes'
+                                        })
+
+                                    .pc-form-grid-col-60
+                                        +form-field__checkbox({
+                                            label: 'Metrics enabled',
+                                            model: '$item.metricsEnabled',
+                                            name: '"MemoryPolicyMetricsEnabled"',
+                                            tip: 'Whether memory metrics are enabled by default on node startup'
+                                        })
+
+                                list-editable-no-items
+                                    list-editable-add-item-button(
+                                        add-item=`$ctrl.Clusters.addMemoryPolicy($ctrl.clonedCluster)`
+                                        label-single='memory policy configuration'
+                                        label-multiple='memory policy configurations'
+                                    )
+
+                    +clusters-memory-policies
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterMemory')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/metrics.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/metrics.pug
new file mode 100644
index 0000000..cfc957e
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/metrics.pug
@@ -0,0 +1,71 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'metrics'
+-var model = '$ctrl.clonedCluster'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Metrics
+    panel-description Cluster runtime metrics settings.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Expire time:',
+                    model: `${model}.metricsExpireTime`,
+                    name: '"metricsExpireTime"',
+                    placeholder: 'Long.MAX_VALUE',
+                    min: '0',
+                    tip: 'Time in milliseconds after which a certain metric value is considered expired'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'History size:',
+                    model: `${model}.metricsHistorySize`,
+                    name: '"metricsHistorySize"',
+                    placeholder: '10000',
+                    min: '1',
+                    tip: 'Number of metrics kept in history to compute totals and averages'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Log frequency:',
+                    model: `${model}.metricsLogFrequency`,
+                    name: '"metricsLogFrequency"',
+                    placeholder: '60000',
+                    min: '0',
+                    tip: 'Frequency of metrics log print out<br/>\ ' +
+                    'When <b>0</b> log print of metrics is disabled'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Update frequency:',
+                    model: `${model}.metricsUpdateFrequency`,
+                    name: '"metricsUpdateFrequency"',
+                    placeholder: '2000',
+                    min: '-1',
+                    tip: 'Job metrics update frequency in milliseconds\
+                        <ul>\
+                            <li>If set to -1 job metrics are never updated</li>\
+                            <li>If set to 0 job metrics are updated on each job start and finish</li>\
+                            <li>Positive value defines the actual update frequency</li>\
+                        </ul>'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterMetrics')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/misc.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/misc.pug
new file mode 100644
index 0000000..bda42eb
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/misc.pug
@@ -0,0 +1,223 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'misc'
+-var model = '$ctrl.clonedCluster'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Miscellaneous
+    panel-description Various miscellaneous cluster settings.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'Work directory:',
+                    model: `${model}.workDirectory`,
+                    name: '"workDirectory"',
+                    placeholder: 'Input work directory',
+                    tip: 'Ignite work directory.<br/>\
+                          If not provided, the method will use work directory under IGNITE_HOME specified by IgniteConfiguration#setIgniteHome(String)\
+                          or IGNITE_HOME environment variable or system property.'
+                })
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'Ignite home:',
+                    model: `${model}.igniteHome`,
+                    name: '"igniteHome"',
+                    placeholder: 'Input ignite home directory',
+                    tip: 'Ignite installation folder'
+                })
+            .pc-form-grid-col-60
+                mixin life-cycle-beans()
+                    .ignite-form-field
+                        -let items = `${model}.lifecycleBeans`;
+
+                        list-editable(
+                            ng-model=items
+                            list-editable-cols=`::[{
+                                name: 'Lifecycle beans:',
+                                tip: 'Collection of life-cycle beans.\
+                                These beans will be automatically notified of grid life-cycle events'
+                            }]`
+                        )
+                            list-editable-item-view {{ $item }}
+
+                            list-editable-item-edit
+                                +list-java-class-field('Bean', '$item', '"bean"', items)
+                                    +form-field__error({
+                                        error: 'igniteUnique',
+                                        message: 'Bin with such class name already configured!'
+                                    })
+
+                            list-editable-no-items
+                                list-editable-add-item-button(
+                                    add-item=`$editLast((${items} = ${items} || []).push(""))`
+                                    label-single='Bean'
+                                    label-multiple='Beans'
+                                )
+
+                - var form = '$parent.form'
+                +life-cycle-beans
+                - var form = 'misc'
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Address resolver:',
+                    model: `${model}.addressResolver`,
+                    name: '"discoAddressResolver"',
+                    tip: 'Address resolver for addresses mapping determination'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'MBean server:',
+                    model: `${model}.mBeanServer`,
+                    name: '"mBeanServer"',
+                    tip: 'MBean server'
+                })
+            .pc-form-grid-col-60
+                .ignite-form-field
+                    +list-text-field({
+                        items: `${model}.includeProperties`,
+                        lbl: 'Include properties',
+                        name: 'includeProperties',
+                        itemName: 'property',
+                        itemsName: 'properties'
+                    })(
+                    list-editable-cols=`::[{
+                            name: 'Include properties:',
+                            tip: 'System or environment property names to include into node attributes'
+                        }]`
+                    )
+                        +form-field__error({error: 'igniteUnique', message: 'Such property already exists!'})
+            .pc-form-grid-col-60
+                mixin store-session-listener-factories()
+                    .ignite-form-field
+                        -let items = `${model}.cacheStoreSessionListenerFactories`;
+
+                        list-editable(
+                            ng-model=items
+                            list-editable-cols=`::[{
+                                name: 'Store session listener factories:',
+                                tip: 'Default store session listener factories for all caches'
+                            }]`
+                        )
+                            list-editable-item-view {{ $item }}
+
+                            list-editable-item-edit
+                                +list-java-class-field('Listener', '$item', '"Listener"', items)
+                                    +form-field__error({
+                                        error: 'igniteUnique',
+                                        message: 'Listener with such class name already exists!'
+                                    })
+
+                            list-editable-no-items
+                                list-editable-add-item-button(
+                                    add-item=`$editLast((${items} = ${items} || []).push(""))`
+                                    label-single='listener'
+                                    label-multiple='listeners'
+                                )
+
+                - var form = '$parent.form'
+                +store-session-listener-factories
+                - var form = 'misc'
+            //- Since ignite 2.0
+            .pc-form-grid-col-60(ng-if-start='$ctrl.available("2.0.0")')
+                +form-field__text({
+                    label: 'Consistent ID:',
+                    model: `${model}.consistentId`,
+                    name: '"ConsistentId"',
+                    placeholder: 'Input consistent ID',
+                    tip: 'Consistent globally unique node ID which survives node restarts'
+                })
+            .pc-form-grid-col-60(ng-if-end)
+                +form-field__java-class({
+                    label: 'Warmup closure:',
+                    model: `${model}.warmupClosure`,
+                    name: '"warmupClosure"',
+                    tip: 'This closure will be executed before actual grid instance start'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.1.0")')
+                +form-field__number({
+                    label: 'Long query timeout:',
+                    model: `${model}.longQueryWarningTimeout`,
+                    name: '"LongQueryWarningTimeout"',
+                    placeholder: '3000',
+                    min: '0',
+                    tip: 'Timeout in milliseconds after which long query warning will be printed'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.7.0")')
+                .ignite-form-field
+                    +list-text-field({
+                        items: `${model}.sqlSchemas`,
+                        lbl: 'SQL schemas',
+                        name: 'sqlSchemas',
+                        itemName: 'schema',
+                        itemsName: 'schemas'
+                    })(
+                    list-editable-cols=`::[{
+                            name: 'SQL schemas:',
+                            tip: 'SQL schemas to be created on node startup.<br/>
+                            Schemas are created on local node only and are not propagated to other cluster nodes.<br/>
+                            Created schemas cannot be dropped.'
+                        }]`
+                    )
+                        +form-field__error({error: 'igniteUnique', message: 'Such property already exists!'})
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.8.0")')
+                +form-field__number({
+                    label: 'SQL query history size:',
+                    model: `${model}.sqlQueryHistorySize`,
+                    name: '"sqlQueryHistorySize"',
+                    placeholder: '1000',
+                    min: '0',
+                    tip: 'Number of SQL query history elements to keep in memory.'
+                })
+            .pc-form-grid-col-60(ng-if-start='$ctrl.available("2.0.0")')
+                +form-field__checkbox({
+                    label: 'Active on start',
+                    model: model + '.activeOnStart',
+                    name: '"activeOnStart"',
+                    tip: 'If cluster is not active on start, there will be no cache partition map exchanges performed until the cluster is activated'
+                })
+            .pc-form-grid-col-60(ng-if-end)
+                +form-field__checkbox({
+                    label: 'Cache sanity check enabled',
+                    model: model + '.cacheSanityCheckEnabled',
+                    name: '"cacheSanityCheckEnabled"',
+                    tip: 'If enabled, then Ignite will perform the following checks and throw an exception if check fails<br/>\
+                        <ul>\
+                            <li>Cache entry is not externally locked with lock or lockAsync methods when entry is enlisted to transaction</li>\
+                            <li>Each entry in affinity group - lock transaction has the same affinity key as was specified on affinity transaction start</li>\
+                            <li>Each entry in partition group - lock transaction belongs to the same partition as was specified on partition transaction start</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.4.0")')
+                +form-field__checkbox({
+                    label: 'Auto activation enabled',
+                    model: model + '.autoActivationEnabled',
+                    name: '"autoActivationEnabled"',
+                    tip: 'Cluster is enabled to activate automatically when all nodes from the BaselineTopology join the cluster'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available(["1.0.0", "2.1.0"])')
+                +form-field__checkbox({
+                    label: 'Late affinity assignment',
+                    model: model + '.lateAffinityAssignment',
+                    name: '"lateAffinityAssignment"',
+                    tip: 'With late affinity assignment mode if primary node was changed for some partition this nodes becomes primary only when rebalancing for all assigned primary partitions is finished'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterMisc', 'caches')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/mvcc.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/mvcc.pug
new file mode 100644
index 0000000..17e8b21
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/mvcc.pug
@@ -0,0 +1,47 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'mvcc'
+-var model = '$ctrl.clonedCluster'
+
+panel-collapsible(ng-show='$ctrl.available("2.7.0")' ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Multiversion concurrency control (MVCC)
+    panel-description Multiversion concurrency control (MVCC) configuration.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Vacuum thread pool size:',
+                    model: `${model}.mvccVacuumThreadCount`,
+                    name: '"MvccVacuumThreadCount"',
+                    placeholder: '2',
+                    min: '0',
+                    tip: 'Number of MVCC vacuum cleanup threads'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Vacuum intervals:',
+                    model: `${model}.mvccVacuumFrequency`,
+                    name: '"MvccVacuumFrequency"',
+                    placeholder: '5000',
+                    min: '0',
+                    tip: 'Time interval between vacuum runs in ms'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterMvcc')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/odbc.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/odbc.pug
new file mode 100644
index 0000000..6c06fc8
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/odbc.pug
@@ -0,0 +1,106 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'odbcConfiguration'
+-var model = '$ctrl.clonedCluster.odbc'
+-var enabled = model + '.odbcEnabled'
+
+panel-collapsible(
+    ng-form=form
+    on-open=`ui.loadPanel('${form}')`
+    ng-show='$ctrl.available(["1.0.0", "2.1.0"])'
+)
+    panel-title ODBC configuration
+    panel-description
+        | ODBC server configuration.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/odbc-driver" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`$ctrl.available(["1.0.0", "2.1.0"]) && ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enabled',
+                    model: enabled,
+                    name: '"odbcEnabled"',
+                    tip: 'Flag indicating whether to configure ODBC configuration'
+                })(
+                    ui-validate=`{
+                        correctMarshaller: '$ctrl.Clusters.odbc.odbcEnabled.correctMarshaller($ctrl.clonedCluster, $value)'
+                    }`
+                    ui-validate-watch='$ctrl.Clusters.odbc.odbcEnabled.correctMarshallerWatch("$ctrl.clonedCluster")'
+                )
+                    +form-field__error({ error: 'correctMarshaller', message: 'ODBC can only be used with BinaryMarshaller' })
+            .pc-form-grid-col-60
+                +form-field__ip-address-with-port-range({
+                    label: `${model}.endpointAddress`,
+                    model: '$item.localOutboundHost',
+                    name: '"endpointAddress"',
+                    enabled,
+                    placeholder: '0.0.0.0:10800..10810',
+                    tip: 'ODBC endpoint address. <br/>\
+                          The following address formats are permitted:\
+                          <ul>\
+                              <li>hostname - will use provided hostname and default port range</li>\
+                              <li>hostname:port - will use provided hostname and port</li>\
+                              <li>hostname:port_from..port_to - will use provided hostname and port range</li>\
+                          </ul>'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Send buffer size:',
+                    model: `${model}.socketSendBufferSize`,
+                    name: '"ODBCSocketSendBufferSize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Socket send buffer size.<br/>\
+                          When set to <b>0</b>, operation system default will be used'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label:'Socket receive buffer size:',
+                    model: `${model}.socketReceiveBufferSize`,
+                    name: '"ODBCSocketReceiveBufferSize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Socket receive buffer size.<br/>\
+                          When set to <b>0</b>, operation system default will be used'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Maximum open cursors',
+                    model: `${model}.maxOpenCursors`,
+                    name: '"maxOpenCursors"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '128',
+                    min: '1',
+                    tip: 'Maximum number of opened cursors per connection'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Pool size:',
+                    model: `${model}.threadPoolSize`,
+                    name: '"ODBCThreadPoolSize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'max(8, availableProcessors)',
+                    min: '1',
+                    tip: 'Size of thread pool that is in charge of processing ODBC tasks'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterODBC')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/persistence.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/persistence.pug
new file mode 100644
index 0000000..e0f5955
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/persistence.pug
@@ -0,0 +1,247 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'persistenceConfiguration'
+-var model = '$ctrl.clonedCluster.persistenceStoreConfiguration'
+-var enabled = model + '.enabled'
+
+panel-collapsible(
+    ng-form=form
+    on-open=`ui.loadPanel('${form}')`
+    ng-show='$ctrl.available(["2.1.0", "2.3.0"])'
+)
+    panel-title Persistence store
+    panel-description
+        | Configures Apache Ignite Native Persistence.
+        a.link-success(href='https://apacheignite.readme.io/docs/distributed-persistent-store' target='_blank') More info
+    panel-content.pca-form-row(ng-if=`$ctrl.available(["2.1.0", "2.3.0"]) && ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enabled',
+                    model: enabled,
+                    name: '"PersistenceEnabled"',
+                    tip: 'Flag indicating whether to configure persistent configuration'
+                })
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'Store path:',
+                    model: `${model}.persistentStorePath`,
+                    name: '"PersistenceStorePath"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'Input store path',
+                    tip: 'A path the root directory where the Persistent Store will persist data and indexes'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Metrics enabled',
+                    model: `${model}.metricsEnabled`,
+                    name: '"PersistenceMetricsEnabled"',
+                    disabled: `!${enabled}`,
+                    tip: 'Flag indicating whether persistence metrics collection is enabled'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Always write full pages',
+                    model: `${model}.alwaysWriteFullPages`,
+                    name: '"PersistenceAlwaysWriteFullPages"',
+                    disabled: `!${enabled}`,
+                    tip: 'Flag indicating whether always write full pages'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Checkpointing frequency:',
+                    model: `${model}.checkpointingFrequency`,
+                    name: '"PersistenceCheckpointingFrequency"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '180000',
+                    min: '1',
+                    tip: 'Frequency which is a minimal interval when the dirty pages will be written to the Persistent Store'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Checkpointing page buffer size:',
+                    model: `${model}.checkpointingPageBufferSize`,
+                    name: '"PersistenceCheckpointingPageBufferSize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '268435456',
+                    min: '0',
+                    tip: 'Amount of memory allocated for a checkpointing temporary buffer'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Checkpointing threads:',
+                    model: `${model}.checkpointingThreads`,
+                    name: '"PersistenceCheckpointingThreads"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '1',
+                    min: '1',
+                    tip: 'A number of threads to use for the checkpointing purposes'
+                })
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'WAL mode:',
+                    model: `${model}.walMode`,
+                    name: '"PersistenceWalMode"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'DEFAULT',
+                    options: '[\
+                        {value: "DEFAULT", label: "DEFAULT"},\
+                        {value: "LOG_ONLY", label: "LOG_ONLY"},\
+                        {value: "BACKGROUND", label: "BACKGROUND"},\
+                        {value: "NONE", label: "NONE"}\
+                    ]',
+                    tip: 'Type define behavior wal fsync.\
+                        <ul>\
+                            <li>DEFAULT - full-sync disk writes</li>\
+                            <li>LOG_ONLY - flushes application buffers</li>\
+                            <li>BACKGROUND - does not force application&#39;s buffer flush</li>\
+                            <li>NONE - WAL is disabled</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'WAL store path:',
+                    model: `${model}.walStorePath`,
+                    name: '"PersistenceWalStorePath"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'Input store path',
+                    tip: 'A path to the directory where WAL is stored'
+                })
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'WAL archive path:',
+                    model: `${model}.walArchivePath`,
+                    name: '"PersistenceWalArchivePath"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'Input archive path',
+                    tip: 'A path to the WAL archive directory'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'WAL segments:',
+                    model: `${model}.walSegments`,
+                    name: '"PersistenceWalSegments"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '10',
+                    min: '1',
+                    tip: 'A number of WAL segments to work with'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'WAL segment size:',
+                    model: `${model}.walSegmentSize`,
+                    name: '"PersistenceWalSegmentSize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '67108864',
+                    min: '0',
+                    tip: 'Size of a WAL segment'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'WAL history size:',
+                    model: `${model}.walHistorySize`,
+                    name: '"PersistenceWalHistorySize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '20',
+                    min: '1',
+                    tip: 'A total number of checkpoints to keep in the WAL history'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'WAL flush frequency:',
+                    model: `${model}.walFlushFrequency`,
+                    name: '"PersistenceWalFlushFrequency"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '2000',
+                    min: '1',
+                    tip:'How often will be fsync, in milliseconds. In background mode, exist thread which do fsync by timeout'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'WAL fsync delay:',
+                    model: `${model}.walFsyncDelayNanos`,
+                    name: '"PersistenceWalFsyncDelay"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '1000',
+                    min: '1',
+                    tip: 'WAL fsync delay, in nanoseconds'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'WAL record iterator buffer size:',
+                    model: `${model}.walRecordIteratorBufferSize`,
+                    name: '"PersistenceWalRecordIteratorBufferSize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '67108864',
+                    min: '1',
+                    tip: 'How many bytes iterator read from disk(for one reading), during go ahead WAL'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Lock wait time:',
+                    model: `${model}.lockWaitTime`,
+                    name: '"PersistenceLockWaitTime"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '10000',
+                    min: '1',
+                    tip: 'Time out in second, while wait and try get file lock for start persist manager'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Rate time interval:' ,
+                    model: `${model}.rateTimeInterval`,
+                    name: '"PersistenceRateTimeInterval"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '60000',
+                    min: '1000',
+                    tip: 'The length of the time interval for rate - based metrics. This interval defines a window over which hits will be tracked.'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Thread local buffer size:',
+                    model: `${model}.tlbSize`,
+                    name: '"PersistenceTlbSize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '131072',
+                    min: '1',
+                    tip: 'Define size thread local buffer. Each thread which write to WAL have thread local buffer for serialize recode before write in WAL'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Sub intervals:',
+                    model: `${model}.subIntervals`,
+                    name: '"PersistenceSubIntervals"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '5',
+                    min: '1',
+                    tip: 'Number of sub - intervals the whole rate time interval will be split into to calculate rate - based metrics'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'WAL auto archive after inactivity:',
+                    model: `${model}.walAutoArchiveAfterInactivity`,
+                    name: '"PersistenceWalAutoArchiveAfterInactivity"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '-1',
+                    min: '-1',
+                    tip: 'Time in millis to run auto archiving segment after last record logging'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterPersistence')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/service.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/service.pug
new file mode 100644
index 0000000..9289d8d
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/service.pug
@@ -0,0 +1,114 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'serviceConfiguration'
+-var model = '$ctrl.clonedCluster.serviceConfigurations'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Service configuration
+    panel-description
+        | Service Grid allows for deployments of arbitrary user-defined services on the cluster.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/fault-tolerance" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6
+            mixin clusters-service-configurations
+                .ignite-form-field(ng-init='serviceConfigurationsTbl={type: "serviceConfigurations", model: "serviceConfigurations", focusId: "kind", ui: "failover-table"}')
+                    +form-field__label({ label: 'Service configurations:', name: '"serviceConfigurations"' })
+
+                    -let items = model
+
+                    list-editable.pc-list-editable-with-form-grid(ng-model=items name='serviceConfigurations')
+                        list-editable-item-edit.pc-form-grid-row
+                            .pc-form-grid-col-60
+                                +form-field__text({
+                                    label: 'Name:',
+                                    model: '$item.name',
+                                    name: '"serviceName"',
+                                    required: true,
+                                    placeholder: 'Input service name'
+                                })(
+                                    ui-validate=`{
+                                        uniqueName: '$ctrl.Clusters.serviceConfigurations.serviceConfiguration.name.customValidators.uniqueName($item, ${items})'
+                                    }`
+                                    ui-validate-watch=`"${items}"`
+                                    ui-validate-watch-object-equality='true'
+                                    ng-model-options='{allowInvalid: true}'
+                                )
+                                    +form-field__error({ error: 'uniqueName', message: 'Service with that name is already configured' })
+                            .pc-form-grid-col-60
+                                +form-field__java-class({
+                                    label: 'Service class',
+                                    model: '$item.service',
+                                    name: '"serviceService"',
+                                    required: 'true',
+                                    tip: 'Service implementation class name'
+                                })
+                            .pc-form-grid-col-60
+                                +form-field__number({
+                                    label: 'Max per node count:',
+                                    model: '$item.maxPerNodeCount',
+                                    name: '"ServiceMaxPerNodeCount"',
+                                    placeholder: 'Unlimited',
+                                    min: '0',
+                                    tip: 'Maximum number of deployed service instances on each node.<br/>\
+                                          Zero for unlimited'
+                                })
+                            .pc-form-grid-col-60
+                                +form-field__number({
+                                    label: 'Total count:',
+                                    model: '$item.totalCount',
+                                    name: '"serviceTotalCount"',
+                                    placeholder: 'Unlimited',
+                                    min: '0',
+                                    tip: 'Total number of deployed service instances in the cluster.<br/>\
+                                        Zero for unlimited'
+                                })
+                            .pc-form-grid-col-60
+                                +form-field__dropdown({
+                                    label: 'Cache:',
+                                    model: '$item.cache',
+                                    name: '"serviceCache"',
+                                    placeholder: 'Key-affinity not used',
+                                    placeholderEmpty: 'No caches configured for current cluster',
+                                    options: '$ctrl.servicesCachesMenu',
+                                    tip: 'Cache name used for key-to-node affinity calculation'
+                                })(
+                                    pc-is-in-collection='$ctrl.clonedCluster.caches'
+                                )
+                                    +form-field__error({ error: 'isInCollection', message: `Cluster doesn't have such a cache` })
+                            .pc-form-grid-col-60
+                                +form-field__text({
+                                    label: 'Affinity key:',
+                                    model: '$item.affinityKey',
+                                    name: '"serviceAffinityKey"',
+                                    placeholder: 'Input affinity key',
+                                    tip: 'Affinity key used for key-to-node affinity calculation'
+                                })
+
+                        list-editable-no-items
+                            list-editable-add-item-button(
+                                add-item=`$ctrl.Clusters.addServiceConfiguration($ctrl.clonedCluster)`
+                                label-single='service configuration'
+                                label-multiple='service configurations'
+                            )
+
+            +clusters-service-configurations
+
+        .pca-form-column-6
+            +preview-xml-java('$ctrl.clonedCluster', 'clusterServiceConfiguration', '$ctrl.caches')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/sql-connector.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/sql-connector.pug
new file mode 100644
index 0000000..e8cd21b
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/sql-connector.pug
@@ -0,0 +1,117 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'query'
+-var model = '$ctrl.clonedCluster'
+-var connectionModel = model + '.sqlConnectorConfiguration'
+-var connectionEnabled = connectionModel + '.enabled'
+
+panel-collapsible(
+    ng-form=form
+    on-open=`ui.loadPanel('${form}')`
+    ng-show='$ctrl.available(["2.1.0", "2.3.0"])'
+)
+    panel-title Query configuration
+    //- TODO IGNITE-5415 Add link to documentation.
+    panel-content.pca-form-row(ng-if=`$ctrl.available(["2.1.0", "2.3.0"]) && ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enabled',
+                    model: connectionEnabled,
+                    name: '"SqlConnectorEnabled"',
+                    tip: 'Flag indicating whether to configure SQL connector configuration'
+                })
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'Host:',
+                    model: `${connectionModel}.host`,
+                    name: '"SqlConnectorHost"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: 'localhost'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Port:',
+                    model: `${connectionModel}.port`,
+                    name: '"SqlConnectorPort"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: '10800',
+                    min: '1025'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Port range:',
+                    model: `${connectionModel}.portRange`,
+                    name: '"SqlConnectorPortRange"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: '100',
+                    min: '0'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Socket send buffer size:',
+                    model: `${connectionModel}.socketSendBufferSize`,
+                    name: '"SqlConnectorSocketSendBufferSize"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Socket send buffer size.<br/>\
+                          When set to <b>0</b>, operation system default will be used'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Socket receive buffer size:',
+                    model: `${connectionModel}.socketReceiveBufferSize`,
+                    name: '"SqlConnectorSocketReceiveBufferSize"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Socket receive buffer size.<br/>\
+                         When set to <b>0</b>, operation system default will be used'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Max connection cursors:',
+                    model: `${connectionModel}.maxOpenCursorsPerConnection`,
+                    name: '"SqlConnectorMaxOpenCursorsPerConnection"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: '128',
+                    min: '0',
+                    tip: 'Max number of opened cursors per connection'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Pool size:',
+                    model: `${connectionModel}.threadPoolSize`,
+                    name: '"SqlConnectorThreadPoolSize"',
+                    disabled: `!(${connectionEnabled})`,
+                    placeholder: 'max(8, availableProcessors)',
+                    min: '1',
+                    tip: 'Size of thread pool that is in charge of processing SQL requests'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'TCP_NODELAY option',
+                    model: `${connectionModel}.tcpNoDelay`,
+                    name: '"SqlConnectorTcpNoDelay"',
+                    disabled: `!${connectionEnabled}`
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterQuery')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/ssl.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/ssl.pug
new file mode 100644
index 0000000..dea087e
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/ssl.pug
@@ -0,0 +1,160 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'sslConfiguration'
+-var cluster = '$ctrl.clonedCluster'
+-var enabled = '$ctrl.clonedCluster.sslEnabled'
+-var model = cluster + '.sslContextFactory'
+-var trust = model + '.trustManagers'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title SSL configuration
+    panel-description
+        | Settings for SSL configuration for creating a secure socket layer.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/ssltls" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enabled',
+                    model: enabled,
+                    name: '"sslEnabled"',
+                    tip: 'Flag indicating whether to configure SSL configuration'
+                })
+            .pc-form-grid-col-60
+                +form-field__typeahead({
+                    label: 'Algorithm to create a key manager:',
+                    model: `${model}.keyAlgorithm`,
+                    name: '"keyAlgorithm"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'SumX509',
+                    options: '["SumX509", "X509"]',
+                    tip: 'Sets key manager algorithm that will be used to create a key manager<br/>\
+                         Notice that in most cased default value suites well, however, on Android platform this value need to be set to X509'
+                })
+
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'Key store file:',
+                    model: `${model}.keyStoreFilePath`,
+                    name: '"keyStoreFilePath"',
+                    disabled: `!(${enabled})`,
+                    required: enabled,
+                    placeholder: 'Path to the key store file',
+                    tip: 'Path to the key store file<br/>\
+                          This is a mandatory parameter since ssl context could not be initialized without key manager'
+                })
+            .pc-form-grid-col-30
+                +form-field__typeahead({
+                    label: 'Key store type:',
+                    model: `${model}.keyStoreType`,
+                    name: '"keyStoreType"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'JKS',
+                    options: '["JKS", "PCKS11", "PCKS12"]',
+                    tip: 'Key store type used in context initialization'
+                })
+            .pc-form-grid-col-30
+                +form-field__typeahead({
+                    label: 'Protocol:',
+                    model: `${model}.protocol`,
+                    name: '"protocol"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'TSL',
+                    options: '["TSL", "SSL"]',
+                    tip: 'Protocol for secure transport'
+                })
+            .pc-form-grid-col-60
+                .ignite-form-field
+                    list-editable(
+                        ng-model=trust
+                        name='trustManagers'
+                        list-editable-cols=`::[{name: "Pre-configured trust managers:"}]`
+                        ng-disabled=enabledToDisabled(enabled)
+                        ng-required=`${enabled} && !${model}.trustStoreFilePath`
+                    )
+                        list-editable-item-view {{ $item }}
+
+                        list-editable-item-edit
+                            +list-java-class-field('Trust manager', '$item', '"trustManager"', trust)
+                                +form-field__error({ error: 'igniteUnique', message: 'Such trust manager already exists!' })
+
+                        list-editable-no-items
+                            list-editable-add-item-button(
+                                add-item=`$editLast((${trust} = ${trust} || []).push(''))`
+                                label-single='trust manager'
+                                label-multiple='trust managers'
+                            )
+                    .form-field__errors(
+                        ng-messages=`sslConfiguration.trustManagers.$error`
+                        ng-show=`sslConfiguration.trustManagers.$invalid`
+                    )
+                        +form-field__error({ error: 'required', message: 'Trust managers or trust store file should be configured' })
+
+            .pc-form-grid-col-30(ng-if-start=`!${trust}.length`)
+                +form-field__text({
+                    label: 'Trust store file:',
+                    model: `${model}.trustStoreFilePath`,
+                    name: '"trustStoreFilePath"',
+                    required: `${enabled} && !${trust}.length`,
+                    disabled: enabledToDisabled(enabled),
+                    placeholder: 'Path to the trust store file',
+                    tip: 'Path to the trust store file'
+                })
+                    +form-field__error({ error: 'required', message: 'Trust store file or trust managers should be configured' })
+            .pc-form-grid-col-30(ng-if-end)
+                +form-field__typeahead({
+                    label: 'Trust store type:',
+                    model: `${model}.trustStoreType`,
+                    name: '"trustStoreType"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'JKS',
+                    options: '["JKS", "PCKS11", "PCKS12"]',
+                    tip: 'Trust store type used in context initialization'
+                })
+            .pc-form-grid-col-60(ng-if-start='$ctrl.available("2.7.0")')
+                .ignite-form-field
+                    +list-text-field({
+                        items: `${model}.cipherSuites`,
+                        lbl: 'Cipher suite',
+                        name: 'cipherSuite',
+                        itemName: 'cipher suite',
+                        itemsName: 'cipher suites'
+
+                    })(
+                        list-editable-cols=`::[{name: 'Suites:'}]`
+                        ng-disabled=`!(${enabled})`
+                    )
+                        +form-field__error({error: 'igniteUnique', message: 'Such suite is already configured!'})
+            .pc-form-grid-col-60(ng-if-end)
+                .ignite-form-field
+                    +list-text-field({
+                        items: `${model}.protocols`,
+                        lbl: 'Protocol',
+                        name: 'protocols',
+                        itemName: 'protocol',
+                        itemsName: 'protocols'
+
+                    })(
+                        list-editable-cols=`::[{name: 'Protocols:'}]`
+                        ng-disabled=`!(${enabled})`
+                    )
+                        +form-field__error({error: 'igniteUnique', message: 'Such protocol already exists!'})
+        .pca-form-column-6
+            +preview-xml-java(cluster, 'clusterSsl')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/swap.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/swap.pug
new file mode 100644
index 0000000..cd19366
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/swap.pug
@@ -0,0 +1,105 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'swap'
+-var model = '$ctrl.clonedCluster'
+-var swapModel = model + '.swapSpaceSpi'
+-var fileSwapModel = swapModel + '.FileSwapSpaceSpi'
+
+panel-collapsible(
+    ng-form=form
+    on-open=`ui.loadPanel('${form}')`
+    ng-show='$ctrl.available(["1.0.0", "2.0.0"])'
+)
+    panel-title Swap
+    panel-description
+        | Settings for overflow data to disk if it cannot fit in memory.
+        | #[a.link-success(href="https://apacheignite.readme.io/v1.9/docs/off-heap-memory#swap-space" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`$ctrl.available(["1.0.0", "2.0.0"]) && ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'Swap space SPI:',
+                    model: `${swapModel}.kind`,
+                    name: '"swapSpaceSpi"',
+                    placeholder: 'Choose swap SPI',
+                    options: '::$ctrl.Clusters.swapSpaceSpis',
+                    tip: 'Provides a mechanism in grid for storing data on disk<br/>\
+                        Ignite cache uses swap space to overflow data to disk if it cannot fit in memory\
+                        <ul>\
+                            <li>File-based swap - File-based swap space SPI implementation which holds keys in memory</li>\
+                            <li>Not set - File-based swap space SPI with default configuration when it needed</li>\
+                        </ul>'
+                })
+            .pc-form-group.pc-form-grid-row(ng-show=`${swapModel}.kind`)
+                .pc-form-grid-col-60
+                    +form-field__text({
+                        label: 'Base directory:',
+                        model: `${fileSwapModel}.baseDirectory`,
+                        name: '"baseDirectory"',
+                        placeholder: 'swapspace',
+                        tip: 'Base directory where to write files'
+                    })
+                .pc-form-grid-col-30
+                    +form-field__number({
+                        label: 'Read stripe size:',
+                        model: `${fileSwapModel}.readStripesNumber`,
+                        name: '"readStripesNumber"',
+                        placeholder: '{{ ::$ctrl.Clusters.swapSpaceSpi.readStripesNumber.default }}',
+                        tip: 'Read stripe size defines number of file channels to be used concurrently'
+                    })(
+                        ui-validate=`{
+                            powerOfTwo: '$ctrl.Clusters.swapSpaceSpi.readStripesNumber.customValidators.powerOfTwo($value)'
+                        }`
+                    )
+                        +form-field__error({ error: 'powerOfTwo', message: 'Read stripe size must be positive and power of two' })
+                .pc-form-grid-col-30
+                    +form-field__number({
+                        label: 'Maximum sparsity:',
+                        model: `${fileSwapModel}.maximumSparsity`,
+                        name: '"maximumSparsity"',
+                        placeholder: '0.5',
+                        min: '0',
+                        max: '0.999',
+                        step: '0.001',
+                        tip: 'This property defines maximum acceptable wasted file space to whole file size ratio<br/>\
+                             When this ratio becomes higher than specified number compacting thread starts working'
+                    })
+                .pc-form-grid-col-30
+                    +form-field__number({
+                        label: 'Max write queue size:',
+                        model: `${fileSwapModel}.maxWriteQueueSize`,
+                        name: '"maxWriteQueueSize"',
+                        placeholder: '1024 * 1024',
+                        min: '0',
+                        tip: 'Max write queue size in bytes<br/>\
+                              If there are more values are waiting for being written to disk then specified size, SPI will block on store operation'
+                    })
+                .pc-form-grid-col-30
+                    +form-field__number({
+                        label: 'Write buffer size:',
+                        model: `${fileSwapModel}.writeBufferSize`,
+                        name: '"writeBufferSize"',
+                        placeholder: '64 * 1024',
+                        min: '0',
+                        tip: 'Write buffer size in bytes<br/>\
+                              Write to disk occurs only when this buffer is full'
+                    })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterSwap')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/thread.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/thread.pug
new file mode 100644
index 0000000..f26f3b9
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/thread.pug
@@ -0,0 +1,207 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'pools'
+-var model = '$ctrl.clonedCluster'
+-var executors = model + '.executorConfiguration'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Thread pools size
+    panel-description Settings for node thread pools.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Public:',
+                    model: model + '.publicThreadPoolSize',
+                    name: '"publicThreadPoolSize"',
+                    placeholder: 'max(8, availableProcessors) * 2',
+                    min: '1',
+                    tip: 'Thread pool that is in charge of processing ComputeJob, GridJobs and user messages sent to node'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'System:',
+                    model: `${model}.systemThreadPoolSize`,
+                    name: '"systemThreadPoolSize"',
+                    placeholder: '{{ ::$ctrl.Clusters.systemThreadPoolSize.default }}',
+                    min: '{{ ::$ctrl.Clusters.systemThreadPoolSize.min }}',
+                    tip: 'Thread pool that is in charge of processing internal system messages'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Service:',
+                    model: model + '.serviceThreadPoolSize',
+                    name: '"serviceThreadPoolSize"',
+                    placeholder: 'max(8, availableProcessors) * 2',
+                    min: '1',
+                    tip: 'Thread pool that is in charge of processing proxy invocation'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Management:',
+                    model: model + '.managementThreadPoolSize',
+                    name: '"managementThreadPoolSize"',
+                    placeholder: '4',
+                    min: '1',
+                    tip: 'Thread pool that is in charge of processing internal and Visor ComputeJob, GridJobs'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'IGFS:',
+                    model: model + '.igfsThreadPoolSize',
+                    name: '"igfsThreadPoolSize"',
+                    placeholder: 'availableProcessors',
+                    min: '1',
+                    tip: 'Thread pool that is in charge of processing outgoing IGFS messages'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Rebalance:',
+                    model: `${model}.rebalanceThreadPoolSize`,
+                    name: '"rebalanceThreadPoolSize"',
+                    placeholder: '{{ ::$ctrl.Clusters.rebalanceThreadPoolSize.default }}',
+                    min: '{{ ::$ctrl.Clusters.rebalanceThreadPoolSize.min }}',
+                    max: `{{ $ctrl.Clusters.rebalanceThreadPoolSize.max(${model}) }}`,
+                    tip: 'Max count of threads can be used at rebalancing'
+                })
+                    +form-field__error({ error: 'max', message: 'Rebalance thread pool size should not exceed or be equal to System thread pool size' })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Utility cache:',
+                    model: model + '.utilityCacheThreadPoolSize',
+                    name: '"utilityCacheThreadPoolSize"',
+                    placeholder: 'max(8, availableProcessors)',
+                    min: '1',
+                    tip: 'Default thread pool size that will be used to process utility cache messages'
+                })
+            .pc-form-grid-col-30
+                form-field-size(
+                    label='Utility cache keep alive time:'
+                    ng-model=`${model}.utilityCacheKeepAliveTime`
+                    name='utilityCacheKeepAliveTime'
+                    size-type='seconds'
+                    size-scale-label='s'
+                    tip='Keep alive time of thread pool size that will be used to process utility cache messages'
+                    min='0'
+                    placeholder='{{ 60000 / _s1.value }}'
+                    on-scale-change='_s1 = $event'
+                )
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label:'Async callback:',
+                    model: model + '.asyncCallbackPoolSize',
+                    name: '"asyncCallbackPoolSize"',
+                    placeholder: 'max(8, availableProcessors)',
+                    min: '1',
+                    tip: 'Size of thread pool that is in charge of processing asynchronous callbacks'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Striped:',
+                    model: model + '.stripedPoolSize',
+                    name: '"stripedPoolSize"',
+                    placeholder: 'max(8, availableProcessors)',
+                    min: '1',
+                    tip: 'Striped pool size that should be used for cache requests processing'
+                })
+
+            //- Since ignite 2.0
+            .pc-form-grid-col-30(ng-if-start='$ctrl.available("2.0.0")')
+                +form-field__number({
+                    label: 'Data streamer:',
+                    model: model + '.dataStreamerThreadPoolSize',
+                    name: '"dataStreamerThreadPoolSize"',
+                    placeholder: 'max(8, availableProcessors)',
+                    min: '1',
+                    tip: 'Size of thread pool that is in charge of processing data stream messages'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Query:',
+                    model: model + '.queryThreadPoolSize',
+                    name: '"queryThreadPoolSize"',
+                    placeholder: 'max(8, availableProcessors)',
+                    min: '1',
+                    tip: 'Size of thread pool that is in charge of processing query messages'
+                })
+            .pc-form-grid-col-60(ng-if-end)
+                .ignite-form-field
+                    +form-field__label({ label: 'Executor configurations:', name: '"executorConfigurations"' })
+                        +form-field__tooltip({ title: `Custom thread pool configurations for compute tasks` })
+
+                    list-editable(
+                        ng-model=executors
+                        ng-model-options='{allowInvalid: true}'
+                        name='executorConfigurations'
+                        ui-validate=`{
+                            allNamesExist: '$ctrl.Clusters.executorConfigurations.allNamesExist($value)',
+                            allNamesUnique: '$ctrl.Clusters.executorConfigurations.allNamesUnique($value)'
+                        }`
+                    )
+                        list-editable-item-view
+                            | {{ $item.name }} /
+                            | {{ $item.size || 'max(8, availableProcessors)'}}
+
+                        list-editable-item-edit
+                            .pc-form-grid-row
+                                .pc-form-grid-col-30
+                                    +form-field__text({
+                                        label: 'Name:',
+                                        model: '$item.name',
+                                        name: '"ExecutorName"',
+                                        required: true,
+                                        placeholder: 'Input executor name',
+                                        tip: 'Thread pool name'
+                                    })(
+                                        ui-validate=`{
+                                            uniqueName: '$ctrl.Clusters.executorConfiguration.name.customValidators.uniqueName($item, ${executors})'
+                                        }`
+                                        ui-validate-watch=`"${executors}"`
+                                        ui-validate-watch-object-equality='true'
+                                        ng-model-options='{allowInvalid: true}'
+                                        ignite-form-field-input-autofocus='true'
+                                    )
+                                        +form-field__error({ error: 'uniqueName', message: 'Service with that name is already configured' })
+                                .pc-form-grid-col-30
+                                    +form-field__number({
+                                        label: 'Pool size:',
+                                        model: '$item.size',
+                                        name: '"ExecutorPoolSize"',
+                                        placeholder: 'max(8, availableProcessors)',
+                                        min: '1',
+                                        tip: 'Thread pool size'
+                                    })
+
+                        list-editable-no-items
+                            list-editable-add-item-button(
+                                add-item=`$edit($ctrl.Clusters.addExecutorConfiguration(${model}))`
+                                label-single='executor configuration'
+                                label-multiple='executor configurations'
+                            )
+
+                    .form-field__errors(
+                        ng-messages=`pools.executorConfigurations.$error`
+                        ng-show=`pools.executorConfigurations.$invalid`
+                    )
+                        +form-field__error({ error: 'allNamesExist', message: 'All executor configurations should have a name' })
+                        +form-field__error({ error: 'allNamesUnique', message: 'All executor configurations should have a unique name' })
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterPools')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/time.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/time.pug
new file mode 100644
index 0000000..ec582ec
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/time.pug
@@ -0,0 +1,72 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'time'
+-var model = '$ctrl.clonedCluster'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Time configuration
+    panel-description Time settings for CLOCK write ordering mode.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            //- Removed in ignite 2.0
+            .pc-form-grid-col-30(ng-if-start='$ctrl.available(["1.0.0", "2.0.0"])')
+                +form-field__number({
+                    label: 'Samples size:',
+                    model: `${model}.clockSyncSamples`,
+                    name: '"clockSyncSamples"',
+                    placeholder: '8',
+                    min: '0',
+                    tip: 'Number of samples used to synchronize clocks between different nodes<br/>\
+                          Clock synchronization is used for cache version assignment in CLOCK order mode'
+                })
+            .pc-form-grid-col-30(ng-if-end)
+                +form-field__number({
+                    label: 'Frequency:',
+                    model: `${model}.clockSyncFrequency`,
+                    name: '"clockSyncFrequency"',
+                    placeholder: '120000',
+                    min: '0',
+                    tip: 'Frequency at which clock is synchronized between nodes, in milliseconds<br/>\
+                          Clock synchronization is used for cache version assignment in CLOCK order mode'
+                })
+
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Port base:',
+                    model: `${model}.timeServerPortBase`,
+                    name: '"timeServerPortBase"',
+                    placeholder: '31100',
+                    min: '0',
+                    max: '65535',
+                    tip: 'Time server provides clock synchronization between nodes<br/>\
+                         Base UPD port number for grid time server. Time server will be started on one of free ports in range'
+                })
+
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Port range:',
+                    model: `${model}.timeServerPortRange`,
+                    name: '"timeServerPortRange"',
+                    placeholder: '100',
+                    min: '1',
+                    tip: 'Time server port range'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterTime')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/transactions.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/transactions.pug
new file mode 100644
index 0000000..29b6e28
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/cluster-edit-form/templates/transactions.pug
@@ -0,0 +1,124 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'transactions'
+-var model = '$ctrl.clonedCluster.transactionConfiguration'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Transactions
+    panel-description
+        | Settings for transactions.
+        | #[a.link-success(href="https://apacheignite.readme.io/docs/transactions" target="_blank") More info]
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-30
+                +form-field__dropdown({
+                    label: 'Concurrency:',
+                    model: `${model}.defaultTxConcurrency`,
+                    name: '"defaultTxConcurrency"',
+                    placeholder: 'PESSIMISTIC',
+                    options: '[\
+                        {value: "OPTIMISTIC", label: "OPTIMISTIC"},\
+                        {value: "PESSIMISTIC", label: "PESSIMISTIC"}\
+                    ]',
+                    tip: 'Cache transaction concurrency to use when one is not explicitly specified\
+                        <ul>\
+                            <li>OPTIMISTIC - All cache operations are not distributed to other nodes until commit is called</li>\
+                            <li>PESSIMISTIC - A lock is acquired on all cache operations with exception of read operations in READ_COMMITTED mode</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-30
+                +form-field__dropdown({
+                    label: 'Isolation:',
+                    model: `${model}.defaultTxIsolation`,
+                    name: '"defaultTxIsolation"',
+                    placeholder: 'REPEATABLE_READ',
+                    options: '[\
+                        {value: "READ_COMMITTED", label: "READ_COMMITTED"},\
+                        {value: "REPEATABLE_READ", label: "REPEATABLE_READ"},\
+                        {value: "SERIALIZABLE", label: "SERIALIZABLE"}\
+                    ]',
+                    tip: 'Default transaction isolation\
+                        <ul>\
+                            <li>READ_COMMITTED - Always a committed value will be provided for read operations</li>\
+                            <li>REPEATABLE_READ - If a value was read once within transaction, then all consecutive reads will provide the same in-transaction value</li>\
+                            <li>SERIALIZABLE - All transactions occur in a completely isolated fashion, as if all transactions in the system had executed serially, one after the other.</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Default timeout:',
+                    model: `${model}.defaultTxTimeout`,
+                    name: '"defaultTxTimeout"',
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Default transaction timeout'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Pessimistic log cleanup delay:',
+                    model: `${model}.pessimisticTxLogLinger`,
+                    name: '"pessimisticTxLogLinger"',
+                    placeholder: '10000',
+                    min: '0',
+                    tip: 'Delay, in milliseconds, after which pessimistic recovery entries will be cleaned up for failed node'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Pessimistic log size:',
+                    model: `${model}.pessimisticTxLogSize`,
+                    name: '"pessimisticTxLogSize"',
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Size of pessimistic transactions log stored on node in order to recover transaction commit if originating node has left grid before it has sent all messages to transaction nodes'
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Manager factory:',
+                    model: `${model}.txManagerFactory`,
+                    name: '"txManagerFactory"',
+                    tip: 'Class name of transaction manager factory for integration with JEE app servers'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.5.0")')
+                +form-field__number({
+                    label: 'Partition map exchange timeout:',
+                    model: `${model}.txTimeoutOnPartitionMapExchange`,
+                    name: '"txTimeoutOnPartitionMapExchange"',
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Transaction timeout for partition map synchronization in milliseconds'
+                })
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.8.0")')
+                +form-field__number({
+                    label: 'Deadlock timeout:',
+                    model: `${model}.deadlockTimeout`,
+                    name: '"deadlockTimeout"',
+                    placeholder: '10000',
+                    min: '0',
+                    tip: 'Timeout before starting deadlock detection for caches configured with TRANSACTIONAL_SNAPSHOT atomicity mode in milliseconds'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Use JTA synchronization',
+                    model: `${model}.useJtaSynchronization`,
+                    name: '"useJtaSynchronization"',
+                    tip: 'Use lightweight JTA synchronization callback to enlist into JTA transaction instead of creating a separate XA resource'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'clusterTransactions')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/component.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/component.ts
new file mode 100644
index 0000000..225a482
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/component.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import controller from './controller';
+import templateUrl from './template.tpl.pug';
+import './style.scss';
+
+export default {
+    controller,
+    templateUrl,
+    bindings: {
+        igfs: '<',
+        igfss: '<',
+        onSave: '&'
+    }
+};
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/controller.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/controller.ts
new file mode 100644
index 0000000..15c9d8c
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/controller.ts
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import cloneDeep from 'lodash/cloneDeep';
+import get from 'lodash/get';
+
+import LegacyConfirmFactory from 'app/services/Confirm.service';
+import Version from 'app/services/Version.service';
+import FormUtilsFactory from 'app/services/FormUtils.service';
+import IGFSs from '../../../../services/IGFSs';
+
+export default class IgfsEditFormController {
+    onSave: ng.ICompiledExpression;
+
+    static $inject = ['IgniteConfirm', 'IgniteVersion', '$scope', 'IGFSs', 'IgniteFormUtils'];
+
+    constructor(
+        private IgniteConfirm: ReturnType<typeof LegacyConfirmFactory>,
+        private IgniteVersion: Version,
+        private $scope: ng.IScope,
+        private IGFSs: IGFSs,
+        private IgniteFormUtils: ReturnType<typeof FormUtilsFactory>
+    ) {}
+
+    $onInit() {
+        this.available = this.IgniteVersion.available.bind(this.IgniteVersion);
+
+        this.$scope.ui = this.IgniteFormUtils.formUI();
+        this.$scope.ui.loadedPanels = ['general', 'secondaryFileSystem', 'misc'];
+
+        this.formActions = [
+            {text: 'Save', icon: 'checkmark', click: () => this.save()},
+            {text: 'Save and Download', icon: 'download', click: () => this.save(true)}
+        ];
+    }
+
+    $onChanges(changes) {
+        if (
+            'igfs' in changes && get(this.$scope.backupItem, '_id') !== get(this.igfs, '_id')
+        ) {
+            this.$scope.backupItem = cloneDeep(changes.igfs.currentValue);
+            if (this.$scope.ui && this.$scope.ui.inputForm) {
+                this.$scope.ui.inputForm.$setPristine();
+                this.$scope.ui.inputForm.$setUntouched();
+            }
+        }
+    }
+    getValuesToCompare() {
+        return [this.igfs, this.$scope.backupItem].map(this.IGFSs.normalize);
+    }
+    save(download) {
+        if (this.$scope.ui.inputForm.$invalid)
+            return this.IgniteFormUtils.triggerValidation(this.$scope.ui.inputForm, this.$scope);
+        this.onSave({$event: {igfs: cloneDeep(this.$scope.backupItem), download}});
+    }
+    reset = (forReal) => forReal ? this.$scope.backupItem = cloneDeep(this.igfs) : void 0;
+    confirmAndReset() {
+        return this.IgniteConfirm.confirm('Are you sure you want to undo all changes for current IGFS?')
+        .then(this.reset);
+    }
+}
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/index.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/index.ts
new file mode 100644
index 0000000..411d0e7
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('configuration.igfs-edit-form', [])
+    .component('igfsEditForm', component);
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/style.scss b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/style.scss
new file mode 100644
index 0000000..881268e
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/style.scss
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+igfs-edit-form {
+    display: block;
+}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/template.tpl.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/template.tpl.pug
new file mode 100644
index 0000000..a85a33c
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/template.tpl.pug
@@ -0,0 +1,35 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+form(id='igfs' name='ui.inputForm' novalidate)
+    include ./templates/general
+
+    include ./templates/secondary
+    include ./templates/ipc
+    include ./templates/fragmentizer
+
+    //- Removed in ignite 2.0
+    include ./templates/dual
+    include ./templates/misc
+
+.pc-form-actions-panel
+    .pc-form-actions-panel__right-after
+    button.btn-ignite.btn-ignite--link-success(
+        type='button'
+        ng-click='$ctrl.confirmAndReset()'
+    )
+        | Cancel
+    pc-split-button(actions=`::$ctrl.formActions`)
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/dual.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/dual.pug
new file mode 100644
index 0000000..c384d61
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/dual.pug
@@ -0,0 +1,59 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'dualMode'
+-var model = 'backupItem'
+
+panel-collapsible(
+    ng-form=form
+    on-open=`ui.loadPanel('${form}')`
+    ng-if='$ctrl.available(["1.0.0", "2.0.0"])'
+)
+    panel-title Dual mode
+    panel-description
+        | IGFS supports dual-mode that allows it to work as either a standalone file system in Hadoop cluster, or work in tandem with HDFS, providing a primary caching layer for the secondary HDFS.
+        | As a caching layer it provides highly configurable read-through and write-through behaviour.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6
+            .settings-row
+                +form-field__number({
+                    label: 'Maximum pending puts size:',
+                    model: `${model}.dualModeMaxPendingPutsSize`,
+                    name: '"dualModeMaxPendingPutsSize"',
+                    placeholder: '0',
+                    min: 'Number.MIN_SAFE_INTEGER',
+                    tip: 'Maximum amount of pending data read from the secondary file system and waiting to be written to data cache<br/>\
+                         Zero or negative value stands for unlimited size'
+                })
+            .settings-row
+                +form-field__java-class({
+                    label: 'Put executor service:',
+                    model: `${model}.dualModePutExecutorService`,
+                    name: '"dualModePutExecutorService"',
+                    tip: 'DUAL mode put operation executor service'
+                })
+            .settings-row
+                +form-field__checkbox({
+                    label: 'Put executor service shutdown',
+                    model: `${model}.dualModePutExecutorServiceShutdown`,
+                    name: '"dualModePutExecutorServiceShutdown"',
+                    tip: 'DUAL mode put operation executor service shutdown flag'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'igfsDualMode')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/fragmentizer.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/fragmentizer.pug
new file mode 100644
index 0000000..047ece5
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/fragmentizer.pug
@@ -0,0 +1,67 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'fragmentizer'
+-var model = 'backupItem'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Fragmentizer
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            -var enabled = `${model}.fragmentizerEnabled`
+
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enabled',
+                    model: enabled,
+                    name: '"fragmentizerEnabled"',
+                    tip: 'Fragmentizer enabled flag'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Concurrent files:',
+                    model: `${model}.fragmentizerConcurrentFiles`,
+                    name: '"fragmentizerConcurrentFiles"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Number of files to process concurrently by fragmentizer'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Throttling block length:',
+                    model: `${model}.fragmentizerThrottlingBlockLength`,
+                    name: '"fragmentizerThrottlingBlockLength"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '16777216',
+                    min: '1',
+                    tip: 'Length of file chunk to transmit before throttling is delayed'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Throttling delay:',
+                    model: `${model}.fragmentizerThrottlingDelay`,
+                    name: '"fragmentizerThrottlingDelay"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '200',
+                    min: '0',
+                    tip: 'Delay in milliseconds for which fragmentizer is paused'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'igfsFragmentizer')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/general.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/general.pug
new file mode 100644
index 0000000..84c2a62
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/general.pug
@@ -0,0 +1,73 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'general'
+-var model = 'backupItem'
+
+panel-collapsible(opened=`::true` ng-form=form)
+    panel-title General
+    panel-description
+        | General IGFS configuration.
+        a.link-success(href="https://apacheignite-fs.readme.io/docs/in-memory-file-system" target="_blank") More info
+    panel-content.pca-form-row
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'Name:',
+                    model: `${model}.name`,
+                    name: '"igfsName"',
+                    placeholder: 'Input name',
+                    required: true
+                })(
+                    ignite-unique='$ctrl.igfss'
+                    ignite-unique-property='name'
+                    ignite-unique-skip=`["_id", ${model}]`
+                )
+                    +form-field__error({ error: 'igniteUnique', message: 'IGFS name should be unique.' })
+            .pc-form-grid-col-30
+                +form-field__dropdown({
+                    label: 'IGFS mode:',
+                    model: `${model}.defaultMode`,
+                    name: '"defaultMode"',
+                    placeholder: '{{::$ctrl.IGFSs.defaultMode.default}}',
+                    options: '{{::$ctrl.IGFSs.defaultMode.values}}',
+                    tip: `
+                    Mode to specify how IGFS interacts with Hadoop file system
+                    <ul>
+                        <li>PRIMARY - in this mode IGFS will not delegate to secondary Hadoop file system and will cache all the files in memory only</li>
+                        <li>PROXY - in this mode IGFS will not cache any files in memory and will only pass them through to secondary file system</li>
+                        <li>DUAL_SYNC - in this mode IGFS will cache files locally and also <b>synchronously</b> write them through to secondary file system</li>
+                        <li>DUAL_ASYNC - in this mode IGFS will cache files locally and also <b> asynchronously </b> write them through to secondary file system</li>
+                    </ul>
+                    `
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Group size:',
+                    model: `${model}.affinnityGroupSize`,
+                    name: '"affinnityGroupSize"',
+                    placeholder: '{{::$ctrl.IGFSs.affinnityGroupSize.default}}',
+                    min: '{{::$ctrl.IGFSs.affinnityGroupSize.min}}',
+                    tip: `
+                        Size of the group in blocks<br/>
+                        Required for construction of affinity mapper in IGFS data cache
+                    `
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'igfsGeneral')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/ipc.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/ipc.pug
new file mode 100644
index 0000000..2dafb71
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/ipc.pug
@@ -0,0 +1,105 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'ipc'
+-var model = 'backupItem'
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title IPC
+    panel-description IGFS Inter-process communication properties.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            -var ipcEndpointConfiguration = `${model}.ipcEndpointConfiguration`
+            -var enabled = `${model}.ipcEndpointEnabled`
+
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enabled',
+                    model: enabled,
+                    name: '"ipcEndpointEnabled"',
+                    tip: 'IPC endpoint enabled flag'
+                })
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'Type:',
+                    model: `${ipcEndpointConfiguration}.type`,
+                    name: '"ipcEndpointConfigurationType"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'TCP',
+                    options: '[\
+                        {value: "SHMEM", label: "SHMEM"},\
+                        {value: "TCP", label: "TCP"}\
+                    ]',
+                    tip: 'IPC endpoint type\
+                        <ul>\
+                            <li>SHMEM - shared memory endpoint</li>\
+                            <li>TCP - TCP endpoint</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-30
+                +form-field__ip-address({
+                    label: 'Host:',
+                    model: `${ipcEndpointConfiguration}.host`,
+                    name: '"ipcEndpointConfigurationHost"',
+                    enabled: enabled,
+                    placeholder: '127.0.0.1',
+                    tip: 'Host endpoint is bound to'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Port:',
+                    model: `${ipcEndpointConfiguration}.port`,
+                    name: '"ipcEndpointConfigurationPort"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '10500',
+                    min: '1',
+                    max: '65535',
+                    tip: 'Port endpoint is bound to'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Memory size:',
+                    model: `${ipcEndpointConfiguration}.memorySize`,
+                    name: '"ipcEndpointConfigurationMemorySize"',
+                    disabled: `!(${enabled})`,
+                    placeholder: '262144',
+                    min: '1',
+                    tip: 'Shared memory size in bytes allocated for endpoint communication'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Thread count:',
+                    model: `${ipcEndpointConfiguration}.threadCount`,
+                    name: '"ipcEndpointConfigurationThreadCount"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'availableProcessors',
+                    min: '1',
+                    tip: 'Number of threads used by this endpoint to process incoming requests'
+                })
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'Token directory:',
+                    model: `${ipcEndpointConfiguration}.tokenDirectoryPath`,
+                    name: '"ipcEndpointConfigurationTokenDirectoryPath"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'ipc/shmem',
+                    tip: 'Directory where shared memory tokens are stored'
+                })
+        .pca-form-column-6
+            +preview-xml-java(model, 'igfsIPC')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/misc.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/misc.pug
new file mode 100644
index 0000000..185b349
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/misc.pug
@@ -0,0 +1,209 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'misc'
+-var model = 'backupItem'
+-var pathModes = `${model}.pathModes`
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Miscellaneous
+    panel-description Various miscellaneous IGFS settings.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Block size:',
+                    model: `${model}.blockSize`,
+                    name: '"blockSize"',
+                    placeholder: '65536',
+                    min: '0',
+                    tip: 'File data block size in bytes'
+                })
+
+            //- Since ignite 2.0
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
+                +form-field__number({
+                    label: 'Buffer size:',
+                    model: `${model}.streamBufferSize`,
+                    name: '"streamBufferSize"',
+                    placeholder: '65536',
+                    min: '0',
+                    tip: 'Read/write buffer size for IGFS stream operations in bytes'
+                })
+
+            //- Removed in ignite 2.0
+            .pc-form-grid-col-60(ng-if-start='$ctrl.available(["1.0.0", "2.0.0"])')
+                +form-field__number({
+                    label: 'Stream buffer size:',
+                    model: `${model}.streamBufferSize`,
+                    name: '"streamBufferSize"',
+                    placeholder: '65536',
+                    min: '0',
+                    tip: 'Read/write buffer size for IGFS stream operations in bytes'
+                })
+            .pc-form-grid-col-60(ng-if-end)
+                +form-field__number({
+                    label: 'Maximum space size:',
+                    model: `${model}.maxSpaceSize`,
+                    name: '"maxSpaceSize"',
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Maximum space available for data cache to store file system entries'
+                })
+
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Maximum task range length:',
+                    model: `${model}.maximumTaskRangeLength`,
+                    name: '"maximumTaskRangeLength"',
+                    placeholder: '0',
+                    min: '0',
+                    tip: 'Maximum default range size of a file being split during IGFS task execution'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Management port:',
+                    model: `${model}.managementPort`,
+                    name: '"managementPort"',
+                    placeholder: '11400',
+                    min: '0',
+                    max: '65535',
+                    tip: 'Port number for management endpoint'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Per node batch size:',
+                    model: `${model}.perNodeBatchSize`,
+                    name: '"perNodeBatchSize"',
+                    placeholder: '100',
+                    min: '0',
+                    tip: 'Number of file blocks collected on local node before sending batch to remote node'
+                })
+            .pc-form-grid-col-30
+                +form-field__number({
+                    label: 'Per node parallel batch count:',
+                    model: `${model}.perNodeParallelBatchCount`,
+                    name: '"perNodeParallelBatchCount"',
+                    placeholder: '8',
+                    min: '0',
+                    tip: 'Number of file block batches that can be concurrently sent to remote node'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Prefetch blocks:',
+                    model: `${model}.prefetchBlocks`,
+                    name: '"prefetchBlocks"',
+                    placeholder: '8',
+                    min: '0',
+                    tip: 'Number of pre-fetched blocks if specific file chunk is requested'
+                })
+            .pc-form-grid-col-60
+                +form-field__number({
+                    label: 'Sequential reads before prefetch:',
+                    model: `${model}.sequentialReadsBeforePrefetch`,
+                    name: '"sequentialReadsBeforePrefetch"',
+                    placeholder: '8',
+                    min: '0',
+                    tip: 'Amount of sequential block reads before prefetch is triggered'
+                })
+
+            //- Removed in ignite 2.0
+            .pc-form-grid-col-60(ng-if='$ctrl.available(["1.0.0", "2.0.0"])')
+                +form-field__number({
+                    label: 'Trash purge timeout:',
+                    model: `${model}.trashPurgeTimeout`,
+                    name: '"trashPurgeTimeout"',
+                    placeholder: '1000',
+                    min: '0',
+                    tip: 'Maximum timeout awaiting for trash purging in case data cache oversize is detected'
+                })
+
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Colocate metadata',
+                    model: `${model}.colocateMetadata`,
+                    name: '"colocateMetadata"',
+                    tip: 'Whether to co-locate metadata on a single node'
+                })
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Relaxed consistency',
+                    model: `${model}.relaxedConsistency`,
+                    name: '"relaxedConsistency"',
+                    tip: 'If value of this flag is <b>true</b>, IGFS will skip expensive consistency checks<br/>\
+                         It is recommended to set this flag to <b>false</b> if your application has conflicting\
+                         operations, or you do not know how exactly users will use your system'
+                })
+
+            //- Since ignite 2.0
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
+                +form-field__checkbox({
+                    label: 'Update file length on flush',
+                    model: model + '.updateFileLengthOnFlush',
+                    name: '"updateFileLengthOnFlush"',
+                    tip: 'Update file length on flush flag'
+                })
+
+            .pc-form-grid-col-60
+                mixin igfs-misc-path-modes
+                    .ignite-form-field
+                        +form-field__label({ label: 'Path modes:', name: '"pathModes"' })
+                            +form-field__tooltip({ title: `Map of path prefixes to IGFS modes used for them` })
+
+                        -let items = pathModes
+
+                        list-editable(ng-model=items)
+                            list-editable-item-view
+                                | {{ $item.path + " [" + $item.mode + "]"}}
+
+                            list-editable-item-edit
+                                - form = '$parent.form'
+
+                                .pc-form-grid-row
+                                    .pc-form-grid-col-30
+                                        +form-field__text({
+                                            label: 'Path:',
+                                            model: '$item.path',
+                                            name: '"path"',
+                                            required: true,
+                                            placeholder: 'Enter path'
+                                        })(ignite-auto-focus)
+                                    .pc-form-grid-col-30
+                                        +form-field__dropdown({
+                                            label: 'Mode:',
+                                            model: `$item.mode`,
+                                            name: '"mode"',
+                                            required: true,
+                                            placeholder: 'Choose igfs mode',
+                                            options: '{{::$ctrl.IGFSs.defaultMode.values}}'
+                                        })(
+                                            ng-model-options='{allowInvalid: true}'
+                                        )
+
+                            list-editable-no-items
+                                list-editable-add-item-button(
+                                    add-item=`$editLast((${items} = ${items} || []).push({}))`
+                                    label-single='path mode'
+                                    label-multiple='path modes'
+                                )
+
+                +igfs-misc-path-modes
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'igfsMisc')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/secondary.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/secondary.pug
new file mode 100644
index 0000000..ac91770
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/igfs-edit-form/templates/secondary.pug
@@ -0,0 +1,307 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'secondaryFileSystem'
+-var model = 'backupItem'
+-var enabled = `${model}.secondaryFileSystemEnabled`
+
+mixin basic-name-mapper(basicModel, prefix)
+    .pc-form-grid-col-60
+        +form-field__text({
+            label: 'Default user name:',
+            model: `${basicModel}.defaultUserName`,
+            name: `"${prefix}BasicDefaultUserName"`,
+            required: `${basicModel}.useDefaultUserName`,
+            disabled: `!(${enabled})`,
+            placeholder: 'Input default user name',
+            tip: 'Default user name'
+        })
+    .pc-form-grid-col-60
+        +form-field__checkbox({
+            label: 'Use default user name',
+            model: `${basicModel}.useDefaultUserName`,
+            name: `"${prefix}BasicUseDefaultUserName"`,
+            required: `!${basicModel}.mappings`,
+            disabled: `!(${enabled})`,
+            tip: 'Whether to use default user name'
+        })
+    .pc-form-grid-col-60
+        .ignite-form-field
+            +form-field__label({label: 'Name mappings:', name: `"${prefix}BasicNameMappings"`})
+                +form-field__tooltip({title: `Maps one user name to another`})
+
+            +list-pair-edit({
+                items: `${basicModel}.mappings`,
+                keyLbl: 'Old name',
+                valLbl: 'New name',
+                itemName: 'name',
+                itemsName: 'names'
+            })
+
+        .form-field__errors(
+            ng-messages=`receiverAddresses.$error`
+            ng-show=`receiverAddresses.$invalid`
+        )
+            +form-field__error({error: 'required', message: 'Name mappings should be configured'})
+
+mixin kerberos-name-mapper(kerberosModel, prefix)
+    .pc-form-grid-col-60
+        +form-field__text({
+            label: 'Instance:',
+            model: `${kerberosModel}.instance`,
+            name: `"${prefix}KerberosInstance"`,
+            disabled: `!(${enabled})`,
+            placeholder: 'Input instance',
+            tip: 'Kerberos instance'
+        })
+    .pc-form-grid-col-60
+        +form-field__text({
+            label: 'Realm:',
+            model: `${kerberosModel}.realm`,
+            name: `"${prefix}KerberosRealm"`,
+            disabled: `!(${enabled})`,
+            placeholder: 'Input realm',
+            tip: 'Kerberos realm'
+        })
+
+mixin custom-name-mapper(customModel, prefix)
+    .pc-form-grid-col-60
+        +form-field__java-class({
+            label: 'Class:',
+            model: `${customModel}.className`,
+            name: `"${prefix}CustomClassName"`,
+            required: true,
+            tip: 'User name mapper implementation class name'
+        })
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Secondary file system
+    panel-description
+        | Secondary file system is provided for pass-through, write-through, and read-through purposes.
+        a.link-success(href="https://apacheignite-fs.readme.io/docs/secondary-file-system" target="_blank") More info
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            -var secondaryFileSystem = `${model}.secondaryFileSystem`
+            -var nameMapperModel = `${secondaryFileSystem}.userNameMapper`
+
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Enabled',
+                    name: '"secondaryFileSystemEnabled"',
+                    model: enabled
+                })(
+                    ng-model-options='{allowInvalid: true}'
+                    ui-validate=`{
+                        requiredWhenIGFSProxyMode: '$ctrl.IGFSs.secondaryFileSystemEnabled.requiredWhenIGFSProxyMode(${model})',
+                        requiredWhenPathModeProxyMode: '$ctrl.IGFSs.secondaryFileSystemEnabled.requiredWhenPathModeProxyMode(${model})'
+                    }`
+                    ui-validate-watch-collection=`"[${model}.defaultMode, ${model}.pathModes]"`
+                    ui-validate-watch-object-equality='true'
+                )
+                    +form-field__error({ error: 'requiredWhenIGFSProxyMode', message: 'Secondary file system should be configured for "PROXY" IGFS mode' })
+                    +form-field__error({ error: 'requiredWhenPathModeProxyMode', message: 'Secondary file system should be configured for "PROXY" path mode' })
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'User name:',
+                    model: `${secondaryFileSystem}.userName`,
+                    name: '"userName"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'Input user name',
+                    tip: 'User name'
+                })
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'File system factory:',
+                    model: `${secondaryFileSystem}.kind`,
+                    name: '"FileSystemFactory"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'Default',
+                    options: '[\
+                        {value: "Caching", label: "Caching"},\
+                        {value: "Kerberos", label: "Kerberos"},\
+                        {value: "Custom", label: "Custom"},\
+                        {value: null, label: "Default"}\
+                    ]',
+                    tip: 'File system factory:\
+                        <ul>\
+                            <li>Caching - Caches File system instances on per-user basis</li>\
+                            <li>Kerberos - Secure Hadoop file system factory that can work with underlying file system protected with Kerberos</li>\
+                            <li>Custom - Custom file system factory</li>\
+                        </ul>'
+                })
+            .pc-form-grid-col-60(ng-if-start=`${secondaryFileSystem}.kind !== "Custom"`)
+                +form-field__text({
+                    label: 'URI:',
+                    model: `${secondaryFileSystem}.uri`,
+                    name: '"hadoopURI"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'hdfs://[namenodehost]:[port]/[path]',
+                    tip: 'URI of file system'
+                })
+            .pc-form-grid-col-60
+                mixin secondary-fs-cfg-paths()
+                    .ignite-form-field
+                        -let items = `${secondaryFileSystem}.cfgPaths`;
+
+                        list-editable(
+                            ng-model=items
+                            list-editable-cols=`::[{
+                                name: 'Config paths:',
+                                tip: 'Additional pathes to Hadoop configurations'
+                            }]`
+                            ng-disabled=`!(${enabled})`
+                        )
+                            list-editable-item-view {{ $item }}
+
+                            list-editable-item-edit
+                                .pc-form-grid-row
+                                    .pc-form-grid-col-60
+                                        +form-field__text({
+                                            label: 'Path:',
+                                            model: '$item',
+                                            name: '"path"',
+                                            required: true,
+                                            placeholder: 'Enter path'
+                                        })(ignite-auto-focus)
+
+                            list-editable-no-items
+                                list-editable-add-item-button(
+                                    add-item=`$editLast((${items} = ${items} || []).push(""))`
+                                    label-single='Config path'
+                                    label-multiple='Config paths'
+                                )
+
+                - var form = '$parent.form'
+                +secondary-fs-cfg-paths
+                - var form = 'secondaryFileSystem'
+            .pc-form-grid-col-60(ng-if-start=`${secondaryFileSystem}.kind === "Kerberos"`)
+                +form-field__text({
+                    label: 'Keytab file:',
+                    model: `${secondaryFileSystem}.Kerberos.keyTab`,
+                    name: '"KerberosKeyTab"',
+                    required: true,
+                    disabled: `!(${enabled})`,
+                    placeholder: 'Input keytab full file name',
+                    tip: 'Keytab full file name'
+                })
+            .pc-form-grid-col-60
+                +form-field__text({
+                    label: 'Keytab principal:',
+                    model: `${secondaryFileSystem}.Kerberos.keyTabPrincipal`,
+                    name: '"KerberosKeyTabPrincipals"',
+                    required: true,
+                    disabled: `!(${enabled})`,
+                    placeholder: 'Input keytab principals',
+                    tip: 'Keytab principals short name'
+                })
+            .pc-form-grid-col-60(ng-if-end)
+                +form-field__number({
+                    label: 'Relogin interval:',
+                    model: `${secondaryFileSystem}.Kerberos.reloginInterval`,
+                    name: '"KerberosReloginInterval"',
+                    placeholder: '600000',
+                    min: '0',
+                    tip: 'Total time in ms for execution of retry attempt'
+                })
+            .pc-form-grid-col-60(ng-if-end)
+                +form-field__dropdown({
+                    label: 'User name mapper:',
+                    model: `${nameMapperModel}.kind`,
+                    name: '"NameMapperKind"',
+                    disabled: `!(${enabled})`,
+                    placeholder: 'Not set',
+                    options: '[\
+                        {value: "Chained", label: "Chained"},\
+                        {value: "Basic", label: "Basic"},\
+                        {value: "Kerberos", label: "Kerberos"},\
+                        {value: "Custom", label: "Custom"},\
+                        {value: null, label: "Not set"}\
+                    ]',
+                    tip: 'File system factory:\
+                        <ul>\
+                            <li>Chained - Delegate name conversion to child mappers</li>\
+                            <li>Basic - Maps one user name to another based on predefined dictionary</li>\
+                            <li>Kerberos - Map simple user name to Kerberos principal</li>\
+                            <li>Custom - Custom user name mapper</li>\
+                        </ul>'
+                })
+            .pc-form-group.pc-form-grid-row(ng-if=`${secondaryFileSystem}.kind !== "Custom" && ${nameMapperModel}.kind === "Chained"`)
+                .pc-form-grid-col-60
+                    .ignite-form-field
+                        -var chainedMapperModel = `${nameMapperModel}.Chained`
+
+                        +form-field__label({label: 'Chained name mappers:', name: '"ChainedNameMappers"'})
+                            +form-field__tooltip({title: `Chained name mappers`})
+
+                        -var items = `${chainedMapperModel}.mappers`
+                        list-editable.pc-list-editable-with-form-grid(ng-model=items name='chainedNameMappers')
+                            list-editable-item-edit.pc-form-grid-row
+                                - form = '$parent.form'
+                                .pc-form-grid-col-60
+                                    +form-field__dropdown({
+                                        label: 'Name mapper:',
+                                        model: '$item.kind',
+                                        name: '"ChainedNameMapperKind"',
+                                        disabled: `!(${enabled})`,
+                                        placeholder: 'Custom',
+                                        options: '[\
+                                            {value: "Basic", label: "Basic"},\
+                                            {value: "Kerberos", label: "Kerberos"},\
+                                            {value: "Custom", label: "Custom"}\
+                                        ]',
+                                        tip: 'File system factory:\
+                                            <ul>\
+                                                <li>Basic - Maps one user name to another based on predefined dictionary</li>\
+                                                <li>Kerberos - Map simple user name to Kerberos principal</li>\
+                                                <li>Custom - Custom user name mapper</li>\
+                                            </ul>'
+                                    })
+                                .pc-form-group.pc-form-grid-row(ng-if=`$item.kind === "Basic"`)
+                                    +basic-name-mapper(`$item.Basic`)
+                                .pc-form-group.pc-form-grid-row(ng-if=`$item.kind === "Kerberos"`)
+                                    +kerberos-name-mapper(`$item.Kerberos`)
+                                .pc-form-group.pc-form-grid-row(ng-if=`$item.kind === "Custom"`)
+                                    +custom-name-mapper(`$item.Custom`, '')
+
+                            list-editable-no-items
+                                list-editable-add-item-button(
+                                    add-item=`$ctrl.IGFSs.addSecondaryFsNameMapper(${model})`
+                                    label-single='name mapper'
+                                    label-multiple='name mappers'
+                                )
+
+            - form = 'secondaryFileSystem'
+            .pc-form-group.pc-form-grid-row(ng-if=`${secondaryFileSystem}.kind !== "Custom" && ${nameMapperModel}.kind === "Basic"`)
+                +basic-name-mapper(`${nameMapperModel}.Basic`)
+            .pc-form-group.pc-form-grid-row(ng-if=`${secondaryFileSystem}.kind !== "Custom" && ${nameMapperModel}.kind === "Kerberos"`)
+                +kerberos-name-mapper(`${nameMapperModel}.Kerberos`)
+            .pc-form-group.pc-form-grid-row(ng-if=`${secondaryFileSystem}.kind !== "Custom" && ${nameMapperModel}.kind === "Custom"`)
+                +custom-name-mapper(`${nameMapperModel}.Custom`, '')
+            .pc-form-grid-col-60(ng-if=`${secondaryFileSystem}.kind === "Custom"`)
+                +form-field__java-class({
+                    label: 'Class:',
+                    model: `${secondaryFileSystem}.Custom.className`,
+                    name: '"customFilesystemFactory"',
+                    required: true,
+                    tip: 'File system factory implementation class name',
+                    validationActive: required
+                })
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'igfsSecondFS')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/component.js b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/component.js
new file mode 100644
index 0000000..56f8677
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/component.js
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import controller from './controller';
+import templateUrl from './template.tpl.pug';
+import './style.scss';
+
+export default {
+    controller,
+    templateUrl,
+    bindings: {
+        model: '<',
+        models: '<',
+        caches: '<',
+        onSave: '&'
+    }
+};
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/controller.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/controller.ts
new file mode 100644
index 0000000..ba6ab89
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/controller.ts
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import cloneDeep from 'lodash/cloneDeep';
+import _ from 'lodash';
+import get from 'lodash/get';
+
+import {default as Models} from '../../../../services/Models';
+import {default as ModalImportModels} from '../../../../components/modal-import-models/service';
+import {default as IgniteVersion} from 'app/services/Version.service';
+import {Confirm} from 'app/services/Confirm.service';
+import {DomainModel} from '../../../../types';
+import ErrorPopover from 'app/services/ErrorPopover.service';
+import LegacyUtilsFactory from 'app/services/LegacyUtils.service';
+import ConfigChangesGuard from '../../../../services/ConfigChangesGuard';
+import FormUtils from 'app/services/FormUtils.service';
+
+export default class ModelEditFormController {
+    model: DomainModel;
+    onSave: ng.ICompiledExpression;
+
+    static $inject = ['ModalImportModels', 'IgniteErrorPopover', 'IgniteLegacyUtils', 'Confirm', 'ConfigChangesGuard', 'IgniteVersion', '$scope', 'Models', 'IgniteFormUtils'];
+
+    constructor(
+        private ModalImportModels: ModalImportModels,
+        private ErrorPopover: ErrorPopover,
+        private LegacyUtils: ReturnType<typeof LegacyUtilsFactory>,
+        private Confirm: Confirm,
+        private ConfigChangesGuard: ConfigChangesGuard,
+        private IgniteVersion: IgniteVersion,
+        private $scope: ng.IScope,
+        private Models: Models,
+        private IgniteFormUtils: ReturnType<typeof FormUtils>
+    ) {}
+
+    javaBuiltInClassesBase = this.LegacyUtils.javaBuiltInClasses;
+
+    $onInit() {
+        this.available = this.IgniteVersion.available.bind(this.IgniteVersion);
+
+        this.queryFieldTypes = this.LegacyUtils.javaBuiltInClasses.concat('byte[]');
+        this.$scope.ui = this.IgniteFormUtils.formUI();
+
+        this.$scope.javaBuiltInClasses = this.LegacyUtils.javaBuiltInClasses;
+        this.$scope.supportedJdbcTypes = this.LegacyUtils.mkOptions(this.LegacyUtils.SUPPORTED_JDBC_TYPES);
+        this.$scope.supportedJavaTypes = this.LegacyUtils.mkOptions(this.LegacyUtils.javaBuiltInTypes);
+
+        this.formActions = [
+            {text: 'Save', icon: 'checkmark', click: () => this.save()},
+            {text: 'Save and Download', icon: 'download', click: () => this.save(true)}
+        ];
+    }
+
+    /**
+     * Create list of fields to show in index fields dropdown.
+     * @param cur Current queryKeyFields
+     */
+    fields(prefix: string, cur: string[]) {
+        const fields = this.$scope.backupItem
+            ? _.map(this.$scope.backupItem.fields, (field) => ({value: field.name, label: field.name}))
+            : [];
+
+        if (prefix === 'new')
+            return fields;
+
+        _.forEach(_.isArray(cur) ? cur : [cur], (value) => {
+            if (!_.find(fields, {value}))
+                fields.push({value, label: value + ' (Unknown field)'});
+        });
+
+        return fields;
+    }
+
+    importModels() {
+        return this.ModalImportModels.open();
+    }
+
+    checkQueryConfiguration(item: DomainModel) {
+        if (item.queryMetadata === 'Configuration' && this.LegacyUtils.domainForQueryConfigured(item)) {
+            if (_.isEmpty(item.fields))
+                return this.ErrorPopover.show('queryFields', 'Query fields should not be empty', this.$scope.ui, 'query');
+
+            const indexes = item.indexes;
+
+            if (indexes && indexes.length > 0) {
+                if (_.find(indexes, (index, idx) => {
+                    if (_.isEmpty(index.fields))
+                        return !this.ErrorPopover.show('indexes' + idx, 'Index fields are not specified', this.$scope.ui, 'query');
+
+                    if (_.find(index.fields, (field) => !_.find(item.fields, (configuredField) => configuredField.name === field.name)))
+                        return !this.ErrorPopover.show('indexes' + idx, 'Index contains not configured fields', this.$scope.ui, 'query');
+                }))
+                    return false;
+            }
+        }
+
+        return true;
+    }
+
+    checkStoreConfiguration(item: DomainModel) {
+        if (this.LegacyUtils.domainForStoreConfigured(item)) {
+            if (this.LegacyUtils.isEmptyString(item.databaseSchema))
+                return this.ErrorPopover.show('databaseSchemaInput', 'Database schema should not be empty', this.$scope.ui, 'store');
+
+            if (this.LegacyUtils.isEmptyString(item.databaseTable))
+                return this.ErrorPopover.show('databaseTableInput', 'Database table should not be empty', this.$scope.ui, 'store');
+
+            if (_.isEmpty(item.keyFields))
+                return this.ErrorPopover.show('keyFields', 'Key fields are not specified', this.$scope.ui, 'store');
+
+            if (this.LegacyUtils.isJavaBuiltInClass(item.keyType) && item.keyFields.length !== 1)
+                return this.ErrorPopover.show('keyFields', 'Only one field should be specified in case when key type is a Java built-in type', this.$scope.ui, 'store');
+
+            if (_.isEmpty(item.valueFields))
+                return this.ErrorPopover.show('valueFields', 'Value fields are not specified', this.$scope.ui, 'store');
+        }
+
+        return true;
+    }
+
+    /**
+     * Check domain model logical consistency.
+     */
+    validate(item: DomainModel) {
+        if (!this.checkQueryConfiguration(item))
+            return false;
+
+        if (!this.checkStoreConfiguration(item))
+            return false;
+
+        if (!this.LegacyUtils.domainForStoreConfigured(item) && !this.LegacyUtils.domainForQueryConfigured(item) && item.queryMetadata === 'Configuration')
+            return this.ErrorPopover.show('query-title', 'SQL query domain model should be configured', this.$scope.ui, 'query');
+
+        if (!this.LegacyUtils.domainForStoreConfigured(item) && item.generatePojo)
+            return this.ErrorPopover.show('store-title', 'Domain model for cache store should be configured when generation of POJO classes is enabled', this.$scope.ui, 'store');
+
+        return true;
+    }
+
+    $onChanges(changes) {
+        if (
+            'model' in changes && get(this.$scope.backupItem, '_id') !== get(this.model, '_id')
+        ) {
+            this.$scope.backupItem = cloneDeep(changes.model.currentValue);
+            if (this.$scope.ui && this.$scope.ui.inputForm) {
+                this.$scope.ui.inputForm.$setPristine();
+                this.$scope.ui.inputForm.$setUntouched();
+            }
+        }
+        if ('caches' in changes)
+            this.cachesMenu = (changes.caches.currentValue || []).map((c) => ({label: c.name, value: c._id}));
+    }
+
+    onQueryFieldsChange(model: DomainModel) {
+        this.$scope.backupItem = this.Models.removeInvalidFields(model);
+    }
+
+    getValuesToCompare() {
+        return [this.model, this.$scope.backupItem].map(this.Models.normalize);
+    }
+
+    save(download) {
+        if (this.$scope.ui.inputForm.$invalid)
+            return this.IgniteFormUtils.triggerValidation(this.$scope.ui.inputForm, this.$scope);
+
+        if (!this.validate(this.$scope.backupItem))
+            return;
+
+        this.onSave({$event: {model: cloneDeep(this.$scope.backupItem), download}});
+    }
+
+    reset = (forReal: boolean) => forReal ? this.$scope.backupItem = cloneDeep(this.model) : void 0;
+
+    confirmAndReset() {
+        return this.Confirm.confirm('Are you sure you want to undo all changes for current model?').then(() => true)
+        .then(this.reset)
+        .catch(() => {});
+    }
+}
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/index.js b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/index.js
new file mode 100644
index 0000000..f9642c9
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/index.js
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('configuration.model-edit-form', [])
+    .component('modelEditForm', component);
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/style.scss b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/style.scss
new file mode 100644
index 0000000..263a51a
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/style.scss
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+model-edit-form {
+    display: block;
+}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/template.tpl.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/template.tpl.pug
new file mode 100644
index 0000000..4751861
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/template.tpl.pug
@@ -0,0 +1,29 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+form(id='model' name='ui.inputForm' novalidate)
+    include ./templates/general
+    include ./templates/query
+    include ./templates/store
+
+.pc-form-actions-panel
+    .pc-form-actions-panel__right-after
+    button.btn-ignite.btn-ignite--link-success(
+        type='button'
+        ng-click='$ctrl.confirmAndReset()'
+    )
+        | Cancel
+    pc-split-button(actions=`::$ctrl.formActions`)
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/templates/general.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/templates/general.pug
new file mode 100644
index 0000000..0481da2
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/templates/general.pug
@@ -0,0 +1,89 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'general'
+-var model = 'backupItem'
+-var generatePojo = `${model}.generatePojo`
+
+panel-collapsible(opened=`::true` ng-form=form)
+    panel-title General
+    panel-description
+        | Domain model properties common for Query and Store.
+        a.link-success(href="https://apacheignite.readme.io/docs/cache-queries" target="_blank") More info about query configuration.
+        a.link-success(href="https://apacheignite.readme.io/docs/3rd-party-store" target="_blank") More info about store.
+    panel-content.pca-form-row
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60
+                +form-field__checkbox({
+                    label: 'Generate POJO classes',
+                    model: generatePojo,
+                    name: '"generatePojo"',
+                    tip: 'If selected then POJO classes will be generated from database tables'
+                })
+            .pc-form-grid-col-30
+                +form-field__dropdown({
+                    label: 'Caches:',
+                    model: `${model}.caches`,
+                    name: '"caches"',
+                    multiple: true,
+                    placeholder: 'Choose caches',
+                    placeholderEmpty: 'No valid caches configured',
+                    options: '$ctrl.cachesMenu',
+                    tip: 'Select caches to describe types in cache'
+                })
+            .pc-form-grid-col-30
+                +form-field__dropdown({
+                    label: 'Query metadata:',
+                    model: `${model}.queryMetadata`,
+                    name: '"queryMetadata"',
+                    required: 'true',
+                    placeholder: '',
+                    options: '::$ctrl.Models.queryMetadata.values',
+                    tip: 'Query metadata configured with:\
+                          <ul>\
+                            <li>Java annotations like @QuerySqlField</li>\
+                            <li>Configuration via QueryEntity class</li>\
+                          </ul>'
+                })
+
+            .pc-form-grid-col-60
+                +form-field__java-class--typeahead({
+                    label: 'Key type:',
+                    model: `${model}.keyType`,
+                    name: '"keyType"',
+                    options: '$ctrl.javaBuiltInClassesBase',
+                    required: 'true',
+                    placeholder: '{{ ' + generatePojo + ' ? "Full class name for Key" : "Key type name" }}',
+                    tip: 'Key class used to store key in cache',
+                    validationActive: generatePojo
+                })
+            .pc-form-grid-col-60
+                +form-field__java-class({
+                    label: 'Value type:',
+                    model: `${model}.valueType`,
+                    name: '"valueType"',
+                    placeholder: '{{ ' + generatePojo +' ? "Enter fully qualified class name" : "Value type name" }}',
+                    tip: 'Value class used to store value in cache',
+                    validationActive: generatePojo
+                })(
+                    ignite-form-field-input-autofocus=autofocus
+                )
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'domainModelGeneral')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/templates/query.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/templates/query.pug
new file mode 100644
index 0000000..9f14980
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/templates/query.pug
@@ -0,0 +1,362 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'query'
+-var model = 'backupItem'
+-var queryKeyFields = `${model}.queryKeyFields`
+-var queryFields = `${model}.fields`
+-var queryAliases = `${model}.aliases`
+-var queryIndexes = `${model}.indexes`
+
+panel-collapsible(ng-form=form opened=`!!${model}.queryMetadata`)
+    panel-title#query-title Domain model for SQL query
+    panel-description
+        | Domain model properties for fields queries.
+        a.link-success(href='https://apacheignite.readme.io/docs/cache-queries' target='_blank') More info
+    panel-content.pca-form-row
+        .pca-form-column-6.pc-form-grid-row
+            .content-not-available(
+                ng-if=`${model}.queryMetadata === 'Annotations'`
+                style='margin-top: 10px'
+            )
+                label Not available for annotated types
+
+            .pc-form-grid-col-60(ng-if-start=`${model}.queryMetadata === 'Configuration'`)
+                +form-field__text({
+                    label: 'Table name:',
+                    model: `${model}.tableName`,
+                    name: '"tableName"',
+                    placeholder: 'Enter table name'
+                })
+
+            .pc-form-grid-col-30(ng-if-start='$ctrl.available("2.0.0")')
+                +form-field__text({
+                    label: 'Key field name:',
+                    model: `${model}.keyFieldName`,
+                    name: '"keyFieldName"',
+                    placeholder: 'Enter key field name',
+                    tip: 'Key name.<br/>' +
+                        'Can be used in field list to denote the key as a whole'
+                })
+            .pc-form-grid-col-30(ng-if-end)
+                +form-field__text({
+                    label: 'Value field name:',
+                    model: `${model}.valueFieldName`,
+                    name: '"valueFieldName"',
+                    placeholder: 'Enter value field name',
+                    tip: 'Value name.<br/>' +
+                        'Can be used in field list to denote the entire value'
+                })
+
+            .pc-form-grid-col-60
+                mixin domains-query-fields
+                    .ignite-form-field
+                        +form-field__label({ label: 'Fields:', name: '"fields"' })
+                            +form-field__tooltip({ title: `Collection of name-to-type mappings to be queried, in addition to indexed fields` })
+
+                        -let items = queryFields
+                        list-editable(
+                            ng-model=items
+                            name='queryFields'
+                            ng-change=`$ctrl.onQueryFieldsChange(${model})`
+                        )
+                            list-editable-item-view
+                                | {{$ctrl.Models.fieldProperties.fieldPresentation($item, $ctrl.available)}}
+
+                            list-editable-item-edit
+                                - form = '$parent.form'
+                                .pc-form-grid-row
+                                    .pc-form-grid-col-30
+                                        +form-field__text({
+                                            label: 'Field name:',
+                                            model: '$item.name',
+                                            name: '"name"',
+                                            required: true,
+                                            placeholder: 'Enter field name'
+                                        })(
+                                            ignite-unique=items
+                                            ignite-unique-property='name'
+                                            ignite-auto-focus
+                                        )
+                                            +form-field__error({ error: 'igniteUnique', message: 'Property with such name already exists!' })
+                                    .pc-form-grid-col-30
+                                        +form-field__java-class--typeahead({
+                                            label: 'Field full class name:',
+                                            model: '$item.className',
+                                            name: '"className"',
+                                            options: '$ctrl.queryFieldTypes',
+                                            required: 'true',
+                                            placeholder: 'Enter field full class name'
+                                        })(
+                                            ng-model-options='{allowInvalid: true}'
+                                            extra-valid-java-identifiers='$ctrl.queryFieldTypes'
+                                        )
+                                    .pc-form-grid-col-60(ng-if='$ctrl.available("2.4.0")')
+                                        +form-field__text({
+                                            label: 'Default value:',
+                                            model: '$item.defaultValue',
+                                            name: '"defaultValue"',
+                                            placeholder: 'Enter default value'
+                                        })
+                                    .pc-form-grid-col-30(ng-if-start='$ctrl.available("2.7.0") && $ctrl.Models.fieldProperties.precisionAvailable($item)')
+                                        +form-field__number({
+                                            label: 'Precision:',
+                                            model: '$item.precision',
+                                            name: '"Precision"',
+                                            placeholder: 'Input field precision',
+                                            min: '1',
+                                            tip: 'Precision of field',
+                                            required: '$item.scale'
+                                        })
+                                    .pc-form-grid-col-30(ng-if-end)
+                                        +form-field__number({
+                                            label: 'Scale:',
+                                            model: '$item.scale',
+                                            name: '"Scale"',
+                                            placeholder: 'input field scale',
+                                            disabled: '!$ctrl.Models.fieldProperties.scaleAvailable($item)',
+                                            min: '0',
+                                            max: '{{$item.precision}}',
+                                            tip: 'Scale of field'
+                                        })
+                                    .pc-form-grid-col-60(ng-if='$ctrl.available("2.3.0")')
+                                        +form-field__checkbox({
+                                            label: 'Not NULL',
+                                            model: '$item.notNull',
+                                            name: '"notNull"',
+                                            tip: 'Field must have non-null value'
+                                        })
+
+                            list-editable-no-items
+                                list-editable-add-item-button(
+                                    add-item=`$editLast((${items} = ${items} || []).push({}))`
+                                    label-single='field to query'
+                                    label-multiple='fields'
+                                )
+
+                +domains-query-fields
+
+            .pc-form-grid-col-60
+                +form-field__dropdown({
+                    label: 'Key fields:',
+                    model: queryKeyFields,
+                    name: '"queryKeyFields"',
+                    multiple: true,
+                    placeholder: 'Select key fields',
+                    placeholderEmpty: 'Configure available fields',
+                    options: `$ctrl.fields('cur', ${queryKeyFields})`,
+                    tip: 'Query fields that belongs to the key.<br/>\
+                     Used to build / modify keys and values during SQL DML operations when no key - value classes are present on cluster nodes.'
+                })
+            .pc-form-grid-col-60
+                mixin domains-query-aliases
+                    .ignite-form-field
+                        +form-field__label({ label: 'Aliases:', name: '"aliases"' })
+                            +form-field__tooltip({ title: `Mapping from full property name in dot notation to an alias that will be used as SQL column name<br />
+                                For example: "parent.name" as "parentName"` })
+
+                        -let items = queryAliases
+
+                        list-editable(ng-model=items name='queryAliases')
+                            list-editable-item-view
+                                | {{ $item.field }} &rarr; {{ $item.alias }}
+
+                            list-editable-item-edit
+                                - form = '$parent.form'
+                                .pc-form-grid-row
+                                    .pc-form-grid-col-30(divider='/')
+                                        +form-field__text({
+                                            label: 'Field name',
+                                            model: '$item.field',
+                                            name: '"field"',
+                                            required: true,
+                                            placeholder: 'Enter field name'
+                                        })(
+                                            ignite-unique=items
+                                            ignite-unique-property='field'
+                                            ignite-auto-focus
+                                        )
+                                            +form-field__error({ error: 'igniteUnique', message: 'Such field already exists!' })
+                                    .pc-form-grid-col-30
+                                        +form-field__text({
+                                            label: 'Field alias',
+                                            model: '$item.alias',
+                                            name: '"alias"',
+                                            required: true,
+                                            placeholder: 'Enter field alias'
+                                        })
+
+                            list-editable-no-items
+                                list-editable-add-item-button(
+                                    add-item=`$editLast((${items} = ${items} || []).push({}))`
+                                    label-single='alias to query'
+                                    label-multiple='aliases'
+                                )
+
+                +domains-query-aliases
+
+            .pc-form-grid-col-60(ng-if-end)
+                .ignite-form-field
+                    +form-field__label({ label: 'Indexes:', name: '"indexes"' })
+
+                    list-editable(
+                        ng-model=queryIndexes
+                        ng-model-options='{allowInvalid: true}'
+                        name='queryIndexes'
+                        ui-validate=`{
+                            complete: '$ctrl.Models.queryIndexes.complete($value)',
+                            fieldsExist: '$ctrl.Models.queryIndexes.fieldsExist($value, ${queryFields})',
+                            indexFieldsHaveUniqueNames: '$ctrl.Models.queryIndexes.indexFieldsHaveUniqueNames($value)'
+                        }`
+                        ui-validate-watch=`"[${queryIndexes}, ${queryFields}]"`
+                        ui-validate-watch-object-equality='true'
+                    )
+                        list-editable-item-view(item-name='queryIndex')
+                            div {{ queryIndex.name }} [{{ queryIndex.indexType }}]
+                            div(ng-repeat='field in queryIndex.fields track by field._id')
+                                span {{ field.name }}
+                                span(ng-if='queryIndex.indexType == "SORTED"')
+                                    |  / {{ field.direction ? 'ASC' : 'DESC'}}
+
+                        list-editable-item-edit(item-name='queryIndex')
+                            .pc-form-grid-row
+                                .pc-form-grid-col-30(divider='/')
+                                    +form-field__text({
+                                        label: 'Index name:',
+                                        model: 'queryIndex.name',
+                                        name: '"name"',
+                                        required: true,
+                                        placeholder: 'Enter index name'
+                                    })(
+                                        ignite-unique=queryIndexes
+                                        ignite-unique-property='name'
+                                        ignite-form-field-input-autofocus='true'
+                                    )
+                                        +form-field__error({ error: 'igniteUnique', message: 'Such index already exists!' })
+                                .pc-form-grid-col-30
+                                    +form-field__dropdown({
+                                        label: 'Index type:',
+                                        model: `queryIndex.indexType`,
+                                        name: '"indexType"',
+                                        required: true,
+                                        placeholder: 'Select index type',
+                                        options: '::$ctrl.Models.indexType.values'
+                                    })
+                                .pc-form-grid-col-60(ng-if='$ctrl.available("2.3.0")')
+                                    +form-field__dropdown({
+                                        label: 'Inline size:',
+                                        model: `queryIndex.inlineSizeType`,
+                                        name: '"InlineSizeKind"',
+                                        placeholder: '{{::$ctrl.Models.inlineSizeType.default}}',
+                                        options: '{{::$ctrl.Models.inlineSizeTypes}}',
+                                        tip: `Inline size
+                                            <ul>
+                                                <li>Auto - Determine inline size automatically</li>
+                                                <li>Custom - Fixed index inline</li>
+                                                <li>Disabled - Index inline is disabled</li>
+                                            </ul>`
+                                    })(
+                                        ng-change=`$ctrl.Models.inlineSizeType.onChange(queryIndex)`
+                                        ng-model-options='{allowInvalid: true}'
+                                    )
+                                .pc-form-grid-col-60(ng-if='$ctrl.available("2.3.0") && queryIndex.inlineSizeType === 1')
+                                    form-field-size(
+                                        label='Inline size:'
+                                        ng-model=`queryIndex.inlineSize`
+                                        ng-model-options='{allowInvalid: true}'
+                                        name=`InlineSize`
+                                        tip='Index inline size in bytes. Part of indexed value will be placed directly to index pages thus minimizing data page accesses'
+                                        placeholder='Input inline size'
+                                        min=`1`
+                                        size-scale-label='kb'
+                                        size-type='bytes'
+                                        required='true'
+                                    )
+                                        +form-field__error({error: 'min', message: 'Inline size should be greater than 0'})
+                                .pc-form-grid-col-60
+                                    .ignite-form-field
+                                        +form-field__label({ label: 'Index fields:', name: '"indexFields"', required: true })
+
+                                        list-editable(
+                                            ng-model='queryIndex.fields'
+                                            ng-model-options='{allowInvalid: true}'
+                                            name='indexFields'
+                                            ng-required='true'
+                                        )
+                                            list-editable-item-view(item-name='indexField')
+                                                | {{ indexField.name }}
+                                                span(ng-if='queryIndex.indexType === "SORTED"')
+                                                    |  / {{ indexField.direction ? "ASC" : "DESC" }}
+
+                                            list-editable-item-edit(item-name='indexField')
+                                                .pc-form-grid-row
+                                                    .pc-form-grid-col-60
+                                                        +form-field__dropdown({
+                                                            label: 'Index field:',
+                                                            model: 'indexField.name',
+                                                            name: '"indexName"',
+                                                            placeholder: `{{ ${queryFields}.length > 0 ? 'Choose index field' : 'No fields configured' }}`,
+                                                            options: queryFields
+                                                        })(
+                                                            bs-options=`queryField.name as queryField.name for queryField in ${queryFields}`
+                                                            ng-disabled=`${queryFields}.length === 0`
+                                                            ng-model-options='{allowInvalid: true}'
+                                                            ignite-unique='queryIndex.fields'
+                                                            ignite-unique-property='name'
+                                                            ignite-auto-focus
+                                                        )
+                                                            +form-field__error({ error: 'igniteUnique', message: 'Such field already exists!' })
+                                                    .pc-form-grid-col-60(
+                                                        ng-if='queryIndex.indexType === "SORTED"'
+                                                    )
+                                                        +form-field__dropdown({
+                                                            label: 'Sort direction:',
+                                                            model: 'indexField.direction',
+                                                            name: '"indexDirection"',
+                                                            required: true,
+                                                            options: '::$ctrl.Models.indexSortDirection.values'
+                                                        })
+                                            list-editable-no-items
+                                                list-editable-add-item-button(
+                                                    add-item=`$edit($ctrl.Models.addIndexField(queryIndex.fields))`
+                                                    label-single='field to index'
+                                                    label-multiple='fields in index'
+                                                )
+                                        .form-field__errors(
+                                            ng-messages=`$form.indexFields.$error`
+                                            ng-show=`$form.indexFields.$invalid`
+                                        )
+                                            +form-field__error({ error: 'required', message: 'Index fields should be configured' })
+
+                        list-editable-no-items
+                            list-editable-add-item-button(
+                                add-item=`$edit($ctrl.Models.addIndex(${model}))`
+                                label-single='index'
+                                label-multiple='fields'
+                            )
+                    .form-field__errors(
+                        ng-messages=`query.queryIndexes.$error`
+                        ng-show=`query.queryIndexes.$invalid`
+                    )
+                        +form-field__error({ error: 'complete', message: 'Some indexes are incomplete' })
+                        +form-field__error({ error: 'fieldsExist', message: 'Some indexes use unknown fields' })
+                        +form-field__error({ error: 'indexFieldsHaveUniqueNames', message: 'Each query index field name should be unique' })
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'domainModelQuery')
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/templates/store.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/templates/store.pug
new file mode 100644
index 0000000..a9f9893
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/model-edit-form/templates/store.pug
@@ -0,0 +1,152 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+
+-var form = 'store'
+-var model = 'backupItem'
+-var keyFields = `${model}.keyFields`
+-var valueFields = `${model}.valueFields`
+
+mixin list-db-field-edit({ items, itemName, itemsName })
+    list-editable(
+        ng-model=items
+        ng-model-options='{allowInvalid: true}'
+        ui-validate=`{
+            dbFieldUnique: '$ctrl.Models.storeKeyDBFieldsUnique($value)'
+        }`
+        ui-validate-watch=`"${items}"`
+        ui-validate-watch-object-equality='true'
+    )&attributes(attributes)
+        list-editable-item-view
+            | {{ $item.databaseFieldName }} / {{ $item.databaseFieldType }} / {{ $item.javaFieldName }} / {{ $item.javaFieldType }}
+
+        list-editable-item-edit
+            .pc-form-grid-row
+                .pc-form-grid-col-30(divider='/')
+                    +form-field__text({
+                        label: 'DB name:',
+                        model: '$item.databaseFieldName',
+                        name: '"databaseFieldName"',
+                        required: true,
+                        placeholder: 'Enter DB name'
+                    })(
+                        ng-model-options='{allowInvalid: true}'
+                        ignite-auto-focus
+                        ignite-unique=items
+                        ignite-unique-property='databaseFieldName'
+                    )
+                        +form-field__error({ error: 'igniteUnique', message: 'DB name should be unique' })
+                .pc-form-grid-col-30
+                    +form-field__dropdown({
+                        label: 'DB type:',
+                        model:'$item.databaseFieldType',
+                        name: '"databaseFieldType"',
+                        required: 'true',
+                        placeholder: 'Choose DB type',
+                        options: 'supportedJdbcTypes'
+                    })
+                .pc-form-grid-col-30(divider='/')
+                    +form-field__text({
+                        label: 'Java name:',
+                        model: '$item.javaFieldName',
+                        name: '"javaFieldName"',
+                        required: true,
+                        placeholder: 'Enter Java name'
+                    })(
+                        ng-model-options='{allowInvalid: true}'
+                        ignite-unique=items
+                        ignite-unique-property='javaFieldName'
+                    )
+                        +form-field__error({ error: 'igniteUnique', message: 'Java name should be unique' })
+                .pc-form-grid-col-30
+                    +form-field__dropdown({
+                        label: 'Java type:',
+                        model: '$item.javaFieldType',
+                        name: '"javaFieldType"',
+                        required: 'true',
+                        placeholder: 'Choose Java type',
+                        options: 'supportedJavaTypes'
+                    })
+
+        list-editable-no-items
+            list-editable-add-item-button(
+                add-item=`$editLast((${items} = ${items} || []).push({}))`
+                label-single=itemName
+                label-multiple=itemsName
+            )
+
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title#store-title Domain model for cache store
+    panel-description
+        | Domain model properties for binding database with cache via POJO cache store.
+        a.link-success(href="https://apacheignite.readme.io/docs/3rd-party-store" target="_blank") More info
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-30
+                +form-field__text({
+                    label: 'Database schema:',
+                    model: model + '.databaseSchema',
+                    name: '"databaseSchema"',
+                    placeholder: 'Input DB schema name',
+                    tip: 'Schema name in database'
+                })
+            .pc-form-grid-col-30
+                +form-field__text({
+                    label: 'Database table:',
+                    model: model + '.databaseTable',
+                    name: '"databaseTable"',
+                    placeholder: 'Input DB table name',
+                    tip: 'Table name in database'
+                })
+            .pc-form-grid-col-60
+                .ignite-form-field
+                    +form-field__label({ label: 'Key fields:', name: '"keyFields"' })
+                        +form-field__tooltip({ title: `Collection of key fields descriptions for CacheJdbcPojoStore` })
+
+                    +list-db-field-edit({
+                        items: keyFields,
+                        itemName: 'key field',
+                        itemsName: 'key fields'
+                    })(name='keyFields')
+
+                    .form-field__errors(
+                        ng-messages=`store.keyFields.$error`
+                        ng-show=`store.keyFields.$invalid`
+                    )
+                        +form-field__error({ error: 'dbFieldUnique', message: 'Each key field DB name and Java name should be unique' })
+
+            .pc-form-grid-col-60
+                .ignite-form-field
+                    +form-field__label({ label: 'Value fields:', name: '"valueFields"' })
+                        +form-field__tooltip({ title: `Collection of value fields descriptions for CacheJdbcPojoStore` })
+
+                    +list-db-field-edit({
+                        items: valueFields,
+                        itemName: 'value field',
+                        itemsName: 'value fields'
+                    })(name='valueFields')
+
+                    .form-field__errors(
+                        ng-messages=`store.valueFields.$error`
+                        ng-show=`store.valueFields.$invalid`
+                    )
+                        +form-field__error({ error: 'dbFieldUnique', message: 'Each value field DB name and Java name should be unique' })
+
+        .pca-form-column-6
+            +preview-xml-java(model, 'domainStore')
+
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-caches/component.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-caches/component.ts
new file mode 100644
index 0000000..daedc4d
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-caches/component.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+
+export default {
+    name: 'pageConfigureAdvancedCaches',
+    template,
+    controller
+};
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-caches/controller.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-caches/controller.ts
new file mode 100644
index 0000000..97ae6e1
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-caches/controller.ts
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {combineLatest, merge, Subject} from 'rxjs';
+import {distinctUntilChanged, map, pluck, publishReplay, refCount, switchMap, tap} from 'rxjs/operators';
+import {StateService, TransitionService, UIRouter} from '@uirouter/angularjs';
+import naturalCompare from 'natural-compare-lite';
+import {advancedSaveCache, removeClusterItems} from '../../../../store/actionCreators';
+import ConfigureState from '../../../../services/ConfigureState';
+import ConfigSelectors from '../../../../store/selectors';
+import Caches from '../../../../services/Caches';
+import Version from 'app/services/Version.service';
+import {ShortCache} from '../../../../types';
+import {IColumnDefOf} from 'ui-grid';
+
+// Controller for Caches screen.
+export default class Controller {
+    static $inject = [
+        'ConfigSelectors',
+        'configSelectionManager',
+        '$uiRouter',
+        '$transitions',
+        'ConfigureState',
+        '$state',
+        'IgniteVersion',
+        'Caches'
+    ];
+
+    constructor(
+        private ConfigSelectors,
+        private configSelectionManager,
+        private $uiRouter: UIRouter,
+        private $transitions: TransitionService,
+        private ConfigureState: ConfigureState,
+        private $state: StateService,
+        private Version: Version,
+        private Caches: Caches
+    ) {}
+
+    visibleRows$ = new Subject();
+    selectedRows$ = new Subject();
+
+    cachesColumnDefs: Array<IColumnDefOf<ShortCache>> = [
+        {
+            name: 'name',
+            displayName: 'Name',
+            field: 'name',
+            enableHiding: false,
+            sort: {direction: 'asc', priority: 0},
+            filter: {
+                placeholder: 'Filter by name…'
+            },
+            sortingAlgorithm: naturalCompare,
+            minWidth: 165
+        },
+        {
+            name: 'cacheMode',
+            displayName: 'Mode',
+            field: 'cacheMode',
+            multiselectFilterOptions: this.Caches.cacheModes,
+            width: 160
+        },
+        {
+            name: 'atomicityMode',
+            displayName: 'Atomicity',
+            field: 'atomicityMode',
+            multiselectFilterOptions: this.Caches.atomicityModes,
+            width: 160
+        },
+        {
+            name: 'backups',
+            displayName: 'Backups',
+            field: 'backups',
+            width: 130,
+            enableFiltering: false,
+            cellTemplate: `
+                <div class="ui-grid-cell-contents">{{ grid.appScope.$ctrl.Caches.getCacheBackupsCount(row.entity) }}</div>
+            `
+        }
+    ];
+
+    $onInit() {
+        const cacheID$ = this.$uiRouter.globals.params$.pipe(
+            pluck('cacheID'),
+            publishReplay(1),
+            refCount()
+        );
+
+        this.shortCaches$ = this.ConfigureState.state$.pipe(this.ConfigSelectors.selectCurrentShortCaches);
+        this.shortModels$ = this.ConfigureState.state$.pipe(this.ConfigSelectors.selectCurrentShortModels);
+        this.shortIGFSs$ = this.ConfigureState.state$.pipe(this.ConfigSelectors.selectCurrentShortIGFSs);
+        this.originalCache$ = cacheID$.pipe(
+            distinctUntilChanged(),
+            switchMap((id) => {
+                return this.ConfigureState.state$.pipe(this.ConfigSelectors.selectCacheToEdit(id));
+            })
+        );
+
+        this.isNew$ = cacheID$.pipe(map((id) => id === 'new'));
+        this.itemEditTitle$ = combineLatest(this.isNew$, this.originalCache$, (isNew, cache) => {
+            return `${isNew ? 'Create' : 'Edit'} cache ${!isNew && cache.name ? `‘${cache.name}’` : ''}`;
+        });
+        this.selectionManager = this.configSelectionManager({
+            itemID$: cacheID$,
+            selectedItemRows$: this.selectedRows$,
+            visibleRows$: this.visibleRows$,
+            loadedItems$: this.shortCaches$
+        });
+
+        this.subscription = merge(
+            this.originalCache$,
+            this.selectionManager.editGoes$.pipe(tap((id) => this.edit(id))),
+            this.selectionManager.editLeaves$.pipe(tap((options) => this.$state.go('base.configuration.edit.advanced.caches', null, options)))
+        ).subscribe();
+
+        this.isBlocked$ = cacheID$;
+
+        this.tableActions$ = this.selectionManager.selectedItemIDs$.pipe(map((selectedItems) => [
+            {
+                action: 'Clone',
+                click: () => this.clone(selectedItems),
+                available: false
+            },
+            {
+                action: 'Delete',
+                click: () => {
+                    this.remove(selectedItems);
+                },
+                available: true
+            }
+        ]));
+    }
+
+    remove(itemIDs: Array<string>) {
+        this.ConfigureState.dispatchAction(
+            removeClusterItems(this.$uiRouter.globals.params.clusterID, 'caches', itemIDs, true, true)
+        );
+    }
+
+    $onDestroy() {
+        this.subscription.unsubscribe();
+        this.visibleRows$.complete();
+        this.selectedRows$.complete();
+    }
+
+    edit(cacheID: string) {
+        this.$state.go('base.configuration.edit.advanced.caches.cache', {cacheID});
+    }
+
+    save({cache, download}) {
+        this.ConfigureState.dispatchAction(advancedSaveCache(cache, download));
+    }
+}
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-caches/index.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-caches/index.ts
new file mode 100644
index 0000000..c6c751c
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-caches/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('ignite-console.page-configure-advanced.caches', [])
+    .component('pageConfigureAdvancedCaches', component);
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-caches/template.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-caches/template.pug
new file mode 100644
index 0000000..ac50b16
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-caches/template.pug
@@ -0,0 +1,57 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+pc-items-table(
+    table-title='::"My caches"'
+    column-defs='$ctrl.cachesColumnDefs'
+    items='$ctrl.shortCaches$|async:this'
+    actions-menu='$ctrl.tableActions$|async:this'
+    selected-row-id='$ctrl.selectionManager.selectedItemIDs$|async:this'
+    one-way-selection='::true'
+    on-selection-change='$ctrl.selectedRows$.next($event)'
+    on-filter-changed='$ctrl.filterChanges$.next($event)'
+    on-visible-rows-change='$ctrl.visibleRows$.next($event)'
+)
+    footer-slot
+        div(style='font-style: italic' ng-hide='($ctrl.shortCaches$|async:this).length')
+            | You have no caches. 
+            a.link-success(
+                ui-sref='base.configuration.edit.advanced.caches.cache({cacheID: "new"})'
+                ui-sref-opts='{location: "replace"}'
+            ) Create one?           
+        a.link-success(
+            ui-sref='base.configuration.edit.advanced.caches.cache({cacheID: "new"})'
+            ui-sref-opts='{location: "replace"}'
+            ng-show='($ctrl.shortCaches$|async:this).length'
+        ) + Add new cache
+
+h2.pc-page-header.ng-animate-disabled(ng-if='!($ctrl.isBlocked$|async:this)')
+    | {{ ($ctrl.selectionManager.selectedItemIDs$|async:this).length ? 'Multiple' : 'No' }} caches selected
+    span.pc-page-header-sub Select only one cache to see settings and edit it
+
+h2.pc-page-header.ng-animate-disabled(ng-if='$ctrl.isBlocked$|async:this')
+    | {{ $ctrl.itemEditTitle$|async:this }}
+
+cache-edit-form(
+    cache='$ctrl.originalCache$|async:this'
+    caches='$ctrl.shortCaches$|async:this'
+    igfss='$ctrl.shortIGFSs$|async:this'
+    models='$ctrl.shortModels$|async:this'
+    on-save='$ctrl.save($event)'
+    ng-class='{"pca-form-blocked": !($ctrl.isBlocked$|async:this)}'
+    fake-ui-can-exit='base.configuration.edit.advanced.caches.cache'
+    form-ui-can-exit-guard
+)
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-cluster/component.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-cluster/component.ts
new file mode 100644
index 0000000..a520146
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-cluster/component.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+
+export default {
+    name: 'pageConfigureAdvancedCluster',
+    template,
+    controller
+};
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-cluster/controller.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-cluster/controller.ts
new file mode 100644
index 0000000..ff9fae9
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-cluster/controller.ts
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {default as ConfigSelectors} from '../../../../store/selectors';
+import {default as ConfigureState} from '../../../../services/ConfigureState';
+import {advancedSaveCluster} from '../../../../store/actionCreators';
+import {distinctUntilChanged, filter, map, pluck, publishReplay, refCount, switchMap, take} from 'rxjs/operators';
+import {UIRouter} from '@uirouter/angularjs';
+
+// Controller for Clusters screen.
+export default class PageConfigureAdvancedCluster {
+    static $inject = ['$uiRouter', 'ConfigSelectors', 'ConfigureState'];
+
+    constructor(
+        private $uiRouter: UIRouter,
+        private ConfigSelectors: ConfigSelectors,
+        private ConfigureState: ConfigureState
+    ) {}
+
+    $onInit() {
+        const clusterID$ = this.$uiRouter.globals.params$.pipe(
+            take(1),
+            pluck('clusterID'),
+            filter((v) => v),
+            take(1)
+        );
+
+        this.shortCaches$ = this.ConfigureState.state$.pipe(this.ConfigSelectors.selectCurrentShortCaches);
+
+        this.originalCluster$ = clusterID$.pipe(
+            distinctUntilChanged(),
+            switchMap((id) => {
+                return this.ConfigureState.state$.pipe(this.ConfigSelectors.selectClusterToEdit(id));
+            }),
+            distinctUntilChanged(),
+            publishReplay(1),
+            refCount()
+        );
+
+        this.isNew$ = this.$uiRouter.globals.params$.pipe(pluck('clusterID'), map((id) => id === 'new'));
+
+        this.isBlocked$ = clusterID$;
+    }
+
+    save({cluster, download}) {
+        this.ConfigureState.dispatchAction(advancedSaveCluster(cluster, download));
+    }
+}
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-cluster/index.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-cluster/index.ts
new file mode 100644
index 0000000..c647937
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-cluster/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('ignite-console.page-configure-advanced.clusters', [])
+    .component(component.name, component);
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-cluster/template.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-cluster/template.pug
new file mode 100644
index 0000000..51ba005
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-cluster/template.pug
@@ -0,0 +1,25 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+cluster-edit-form(
+    is-new='$ctrl.isNew$|async:this'
+    cluster='$ctrl.originalCluster$|async:this'
+    caches='$ctrl.shortCaches$|async:this'
+    on-save='$ctrl.save($event)'
+    ng-class='{"pca-form-blocked": !($ctrl.isBlocked$|async:this)}'
+    fake-ui-can-exit='base.configuration.edit.advanced.cluster'
+    form-ui-can-exit-guard
+)
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-igfs/component.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-igfs/component.ts
new file mode 100644
index 0000000..868e3d0
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-igfs/component.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import controller from './controller';
+import template from './template.pug';
+
+export default {
+    name: 'pageConfigureAdvancedIgfs',
+    template,
+    controller
+};
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-igfs/controller.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-igfs/controller.ts
new file mode 100644
index 0000000..d4b7a46
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-igfs/controller.ts
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {combineLatest, merge, Observable, Subject} from 'rxjs';
+import {distinctUntilChanged, map, pluck, publishReplay, refCount, switchMap, tap} from 'rxjs/operators';
+import naturalCompare from 'natural-compare-lite';
+import get from 'lodash/get';
+import {advancedSaveIGFS, removeClusterItems} from '../../../../store/actionCreators';
+import ConfigureState from '../../../../services/ConfigureState';
+import ConfigSelectors from '../../../../store/selectors';
+import IGFSs from '../../../../services/IGFSs';
+import {StateService, UIRouter} from '@uirouter/angularjs';
+import ConfigSelectionManager from '../../../../services/ConfigSelectionManager';
+import {ShortIGFS} from '../../../../types';
+import {IColumnDefOf} from 'ui-grid';
+
+export default class PageConfigureAdvancedIGFS {
+    static $inject = ['ConfigSelectors', 'ConfigureState', '$uiRouter', 'IGFSs', '$state', 'configSelectionManager'];
+
+    constructor(
+        private ConfigSelectors: ConfigSelectors,
+        private ConfigureState: ConfigureState,
+        private $uiRouter: UIRouter,
+        private IGFSs: IGFSs,
+        private $state: StateService,
+        private configSelectionManager: ReturnType<typeof ConfigSelectionManager>
+    ) {}
+
+    columnDefs: Array<IColumnDefOf<ShortIGFS>>;
+
+    shortItems$: Observable<ShortIGFS>;
+
+    $onDestroy() {
+        this.subscription.unsubscribe();
+        this.visibleRows$.complete();
+        this.selectedRows$.complete();
+    }
+
+    $onInit() {
+        this.visibleRows$ = new Subject();
+        this.selectedRows$ = new Subject();
+
+        this.columnDefs = [
+            {
+                name: 'name',
+                displayName: 'Name',
+                field: 'name',
+                enableHiding: false,
+                filter: {
+                    placeholder: 'Filter by name…'
+                },
+                sort: {direction: 'asc', priority: 0},
+                sortingAlgorithm: naturalCompare,
+                minWidth: 165
+            },
+            {
+                name: 'defaultMode',
+                displayName: 'Mode',
+                field: 'defaultMode',
+                multiselectFilterOptions: this.IGFSs.defaultMode.values,
+                width: 160
+            },
+            {
+                name: 'affinnityGroupSize',
+                displayName: 'Group size',
+                field: 'affinnityGroupSize',
+                enableFiltering: false,
+                width: 130
+            }
+        ];
+
+        this.itemID$ = this.$uiRouter.globals.params$.pipe(pluck('igfsID'));
+
+        this.shortItems$ = this.ConfigureState.state$.pipe(
+            this.ConfigSelectors.selectCurrentShortIGFSs,
+            map((items = []) => items.map((i) => ({
+                _id: i._id,
+                name: i.name,
+                affinnityGroupSize: i.affinnityGroupSize || this.IGFSs.affinnityGroupSize.default,
+                defaultMode: i.defaultMode || this.IGFSs.defaultMode.default
+            })))
+        );
+
+        this.originalItem$ = this.itemID$.pipe(
+            distinctUntilChanged(),
+            switchMap((id) => {
+                return this.ConfigureState.state$.pipe(this.ConfigSelectors.selectIGFSToEdit(id));
+            }),
+            distinctUntilChanged(),
+            publishReplay(1),
+            refCount()
+        );
+
+        this.isNew$ = this.itemID$.pipe(map((id) => id === 'new'));
+
+        this.itemEditTitle$ = combineLatest(this.isNew$, this.originalItem$, (isNew, item) => {
+            return `${isNew ? 'Create' : 'Edit'} IGFS ${!isNew && get(item, 'name') ? `‘${get(item, 'name')}’` : ''}`;
+        });
+
+        this.selectionManager = this.configSelectionManager({
+            itemID$: this.itemID$,
+            selectedItemRows$: this.selectedRows$,
+            visibleRows$: this.visibleRows$,
+            loadedItems$: this.shortItems$
+        });
+
+        this.tableActions$ = this.selectionManager.selectedItemIDs$.pipe(map((selectedItems) => [
+            {
+                action: 'Clone',
+                click: () => this.clone(selectedItems),
+                available: false
+            },
+            {
+                action: 'Delete',
+                click: () => {
+                    this.remove(selectedItems);
+                },
+                available: true
+            }
+        ]));
+
+        this.subscription = merge(
+            this.originalItem$,
+            this.selectionManager.editGoes$.pipe(tap((id) => this.edit(id))),
+            this.selectionManager.editLeaves$.pipe(tap((options) => this.$state.go('base.configuration.edit.advanced.igfs', null, options)))
+        ).subscribe();
+    }
+
+    edit(igfsID) {
+        this.$state.go('base.configuration.edit.advanced.igfs.igfs', {igfsID});
+    }
+
+    save({igfs, download}) {
+        this.ConfigureState.dispatchAction(advancedSaveIGFS(igfs, download));
+    }
+
+    remove(itemIDs) {
+        this.ConfigureState.dispatchAction(
+            removeClusterItems(this.$uiRouter.globals.params.clusterID, 'igfss', itemIDs, true, true)
+        );
+    }
+}
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-igfs/index.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-igfs/index.ts
new file mode 100644
index 0000000..44b50b0
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-igfs/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('ignite-console.page-configure-advanced.igfs', [])
+    .component(component.name, component);
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-igfs/template.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-igfs/template.pug
new file mode 100644
index 0000000..e11b9df
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-igfs/template.pug
@@ -0,0 +1,51 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+pc-items-table(
+    table-title='::"My IGFS"'
+    column-defs='$ctrl.columnDefs'
+    items='$ctrl.shortItems$|async:this'
+    actions-menu='$ctrl.tableActions$|async:this'
+    selected-row-id='$ctrl.selectionManager.selectedItemIDs$|async:this'
+    one-way-selection='::true'
+    on-selection-change='$ctrl.selectedRows$.next($event)'
+    on-filter-changed='$ctrl.filterChanges$.next($event)'
+    on-visible-rows-change='$ctrl.visibleRows$.next($event)'
+)
+    footer-slot
+        div(style='font-style: italic' ng-hide='($ctrl.shortItems$|async:this).length')
+            | You have no IGFS. #[a.link-success(ui-sref='base.configuration.edit.advanced.igfs.igfs({igfsID: "new"})') Create one?]
+        a.link-success(
+            ui-sref='base.configuration.edit.advanced.igfs.igfs({igfsID: "new"})'
+            ng-show='($ctrl.shortItems$|async:this).length'
+        ) + Add new IGFS
+
+h2.pc-page-header.ng-animate-disabled(ng-if='!($ctrl.itemID$|async:this)')
+    | {{ ($ctrl.selectionManager.selectedItemIDs$|async:this).length ? 'Multiple' : 'No' }} IGFSs selected
+    span.pc-page-header-sub Select only one IGFS to see settings and edit it
+
+h2.pc-page-header.ng-animate-disabled(ng-if='$ctrl.itemID$|async:this')
+    | {{ $ctrl.itemEditTitle$|async:this }}
+
+igfs-edit-form(
+    igfs='$ctrl.originalItem$|async:this'
+    igfss='$ctrl.shortItems$|async:this'
+    m_odels='$ctrl.shortModels$|async:this'
+    on-save='$ctrl.save($event)'
+    ng-class='{"pca-form-blocked": !($ctrl.itemID$|async:this)}'
+    fake-ui-can-exit='base.configuration.edit.advanced.igfs.igfs'
+    form-ui-can-exit-guard
+)
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/component.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/component.ts
new file mode 100644
index 0000000..f29a940
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/component.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import controller from './controller';
+import template from './template.pug';
+import './style.scss';
+
+export default {
+    name: 'pageConfigureAdvancedModels',
+    template,
+    controller
+};
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/controller.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/controller.ts
new file mode 100644
index 0000000..8394965
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/controller.ts
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {combineLatest, merge, Observable, Subject} from 'rxjs';
+import {distinctUntilChanged, map, pluck, publishReplay, refCount, switchMap, tap} from 'rxjs/operators';
+
+import get from 'lodash/get';
+
+import hasIndexTemplate from './hasIndex.template.pug';
+import keyCellTemplate from './keyCell.template.pug';
+import valueCellTemplate from './valueCell.template.pug';
+
+import {advancedSaveModel, removeClusterItems} from '../../../../store/actionCreators';
+
+import {default as ConfigSelectors} from '../../../../store/selectors';
+import {default as ConfigureState} from '../../../../services/ConfigureState';
+import {default as Models} from '../../../../services/Models';
+
+import {StateService, UIRouter} from '@uirouter/angularjs';
+import {DomainModel, ShortCache, ShortDomainModel} from '../../../../types';
+import {IColumnDefOf} from 'ui-grid';
+import ConfigSelectionManager from '../../../../services/ConfigSelectionManager';
+
+export default class PageConfigureAdvancedModels {
+    static $inject = ['ConfigSelectors', 'ConfigureState', '$uiRouter', 'Models', '$state', 'configSelectionManager'];
+
+    constructor(
+        private ConfigSelectors: ConfigSelectors,
+        private ConfigureState: ConfigureState,
+        private $uiRouter: UIRouter,
+        private Models: Models,
+        private $state: StateService,
+        private configSelectionManager: ReturnType<typeof ConfigSelectionManager>
+    ) {}
+    visibleRows$: Subject<Array<ShortDomainModel>>;
+    selectedRows$: Subject<Array<ShortDomainModel>>;
+    columnDefs: Array<IColumnDefOf<ShortDomainModel>>;
+    itemID$: Observable<string>;
+    shortItems$: Observable<Array<ShortDomainModel>>;
+    shortCaches$: Observable<Array<ShortCache>>;
+    originalItem$: Observable<DomainModel>;
+
+    $onDestroy() {
+        this.subscription.unsubscribe();
+        this.visibleRows$.complete();
+        this.selectedRows$.complete();
+    }
+    $onInit() {
+        this.visibleRows$ = new Subject();
+
+        this.selectedRows$ = new Subject();
+
+        this.columnDefs = [
+            {
+                name: 'hasIndex',
+                displayName: 'Indexed',
+                field: 'hasIndex',
+                type: 'boolean',
+                enableFiltering: true,
+                visible: true,
+                multiselectFilterOptions: [{value: true, label: 'Yes'}, {value: false, label: 'No'}],
+                width: 100,
+                cellTemplate: hasIndexTemplate
+            },
+            {
+                name: 'keyType',
+                displayName: 'Key type',
+                field: 'keyType',
+                enableHiding: false,
+                filter: {
+                    placeholder: 'Filter by key type…'
+                },
+                cellTemplate: keyCellTemplate,
+                minWidth: 165
+            },
+            {
+                name: 'valueType',
+                displayName: 'Value type',
+                field: 'valueType',
+                enableHiding: false,
+                filter: {
+                    placeholder: 'Filter by value type…'
+                },
+                sort: {direction: 'asc', priority: 0},
+                cellTemplate: valueCellTemplate,
+                minWidth: 165
+            }
+        ];
+
+        this.itemID$ = this.$uiRouter.globals.params$.pipe(pluck('modelID'));
+
+        this.shortItems$ = this.ConfigureState.state$.pipe(
+            this.ConfigSelectors.selectCurrentShortModels,
+            tap((shortModels = []) => {
+                const value = shortModels.every((m) => m.hasIndex);
+                this.columnDefs[0].visible = !value;
+            }),
+            publishReplay(1),
+            refCount()
+        );
+
+        this.shortCaches$ = this.ConfigureState.state$.pipe(this.ConfigSelectors.selectCurrentShortCaches);
+
+        this.originalItem$ = this.itemID$.pipe(
+            distinctUntilChanged(),
+            switchMap((id) => {
+                return this.ConfigureState.state$.pipe(this.ConfigSelectors.selectModelToEdit(id));
+            }),
+            distinctUntilChanged(),
+            publishReplay(1),
+            refCount()
+        );
+
+        this.isNew$ = this.itemID$.pipe(map((id) => id === 'new'));
+
+        this.itemEditTitle$ = combineLatest(this.isNew$, this.originalItem$, (isNew, item) => {
+            return `${isNew ? 'Create' : 'Edit'} model ${!isNew && get(item, 'valueType') ? `‘${get(item, 'valueType')}’` : ''}`;
+        });
+
+        this.selectionManager = this.configSelectionManager({
+            itemID$: this.itemID$,
+            selectedItemRows$: this.selectedRows$,
+            visibleRows$: this.visibleRows$,
+            loadedItems$: this.shortItems$
+        });
+
+        this.tableActions$ = this.selectionManager.selectedItemIDs$.pipe(map((selectedItems) => [
+            {
+                action: 'Clone',
+                click: () => this.clone(selectedItems),
+                available: false
+            },
+            {
+                action: 'Delete',
+                click: () => {
+                    this.remove(selectedItems);
+                },
+                available: true
+            }
+        ]));
+
+        this.subscription = merge(
+            this.originalItem$,
+            this.selectionManager.editGoes$.pipe(tap((id) => this.edit(id))),
+            this.selectionManager.editLeaves$.pipe(tap((options) => this.$state.go('base.configuration.edit.advanced.models', null, options)))
+        ).subscribe();
+    }
+
+    edit(modelID) {
+        this.$state.go('base.configuration.edit.advanced.models.model', {modelID});
+    }
+
+    save({model, download}) {
+        this.ConfigureState.dispatchAction(advancedSaveModel(model, download));
+    }
+
+    remove(itemIDs: Array<string>) {
+        this.ConfigureState.dispatchAction(
+            removeClusterItems(this.$uiRouter.globals.params.clusterID, 'models', itemIDs, true, true)
+        );
+    }
+}
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/hasIndex.template.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/hasIndex.template.pug
new file mode 100644
index 0000000..68330a0
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/hasIndex.template.pug
@@ -0,0 +1,23 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.ui-grid-cell-contents(ng-class=`{
+    'page-configure-advanced__invalid-model-cell': !row.entity[col.field],
+    'page-configure-advanced__valid-model-cell': row.entity[col.field],
+}`)
+    svg(ignite-icon='attention' ng-if='!row.entity[col.field]')
+    svg(ignite-icon='checkmark' ng-if='row.entity[col.field]')
+    span {{ row.entity[col.field] ? 'Yes' : 'No'}}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/index.ts b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/index.ts
new file mode 100644
index 0000000..e1a800a
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('ignite-console.page-configure-advanced.models', [])
+    .component(component.name, component);
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/keyCell.template.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/keyCell.template.pug
new file mode 100644
index 0000000..5608448
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/keyCell.template.pug
@@ -0,0 +1,21 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.ui-grid-cell-contents(ng-class=`{'page-configure-advanced__invalid-model-cell': !row.entity.keyType}`)
+    span(ng-if='row.entity[col.field]')
+        | {{ row.entity[col.field] }}
+    i(ng-if-start='!row.entity[col.field]') No keyType defined
+    svg(ignite-icon='attention' ng-if-end)
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/style.scss b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/style.scss
new file mode 100644
index 0000000..8dc6e23
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/style.scss
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+page-configure-advanced-models {
+    @import 'public/stylesheets/variables.scss';
+    .page-configure-advanced__valid-model-cell,
+    .page-configure-advanced__invalid-model-cell {
+        i {
+            font-style: italic;
+        }
+        [ignite-icon] {
+            vertical-align: -3px;
+            margin-right: 10px;
+        }        
+    }
+    .page-configure-advanced__valid-model-cell {
+        color: green;
+    }
+    .page-configure-advanced__invalid-model-cell {
+        color: $ignite-brand-primary;
+
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/template.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/template.pug
new file mode 100644
index 0000000..0586ae1
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/template.pug
@@ -0,0 +1,51 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+pc-items-table(
+    table-title='::"My domain models"'
+    column-defs='$ctrl.columnDefs'
+    items='$ctrl.shortItems$|async:this'
+    actions-menu='$ctrl.tableActions$|async:this'
+    selected-row-id='$ctrl.selectionManager.selectedItemIDs$|async:this'
+    one-way-selection='::true'
+    on-selection-change='$ctrl.selectedRows$.next($event)'
+    on-filter-changed='$ctrl.filterChanges$.next($event)'
+    on-visible-rows-change='$ctrl.visibleRows$.next($event)'
+)
+    footer-slot
+        div(style='font-style: italic' ng-hide='($ctrl.shortItems$|async:this).length')
+            | You have no models. #[a.link-success(ui-sref='base.configuration.edit.advanced.models.model({modelID: "new"})') Create one?]
+        a.link-success(
+            ui-sref='base.configuration.edit.advanced.models.model({modelID: "new"})'
+            ng-show='($ctrl.shortItems$|async:this).length'
+        ) + Add new model
+
+h2.pc-page-header.ng-animate-disabled(ng-if='!($ctrl.itemID$|async:this)')
+    | {{ ($ctrl.selectionManager.selectedItemIDs$|async:this).length ? 'Multiple' : 'No' }} models selected
+    span.pc-page-header-sub Select only one model to see settings and edit it
+
+h2.pc-page-header.ng-animate-disabled(ng-if='$ctrl.itemID$|async:this')
+    | {{ $ctrl.itemEditTitle$|async:this }}
+
+model-edit-form(
+    model='$ctrl.originalItem$|async:this'
+    models='$ctrl.shortItems$|async:this'
+    caches='$ctrl.shortCaches$|async:this'
+    on-save='$ctrl.save($event)'
+    ng-class='{"pca-form-blocked": !($ctrl.itemID$|async:this)}'
+    fake-ui-can-exit='base.configuration.edit.advanced.models.model'
+    form-ui-can-exit-guard
+)
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/valueCell.template.pug b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/valueCell.template.pug
new file mode 100644
index 0000000..6bbe294
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/components/page-configure-advanced-models/valueCell.template.pug
@@ -0,0 +1,18 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.ui-grid-cell-contents(ng-class=`{'page-configure-advanced__invalid-model-cell': !row.entity.keyType}`)
+    | {{ row.entity[col.field]}}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/controller.ts b/modules/frontend/app/configuration/components/page-configure-advanced/controller.ts
new file mode 100644
index 0000000..cbeb57b
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/controller.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class PageConfigureAdvancedController {
+    static menuItems = [
+        { text: 'Cluster', sref: 'base.configuration.edit.advanced.cluster' },
+        { text: 'SQL Scheme', sref: 'base.configuration.edit.advanced.models' },
+        { text: 'Caches', sref: 'base.configuration.edit.advanced.caches' },
+        { text: 'IGFS', sref: 'base.configuration.edit.advanced.igfs' }
+    ];
+
+    menuItems: Array<{text: string, sref: string}>;
+
+    $onInit() {
+        this.menuItems = this.constructor.menuItems;
+    }
+}
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/index.ts b/modules/frontend/app/configuration/components/page-configure-advanced/index.ts
new file mode 100644
index 0000000..9eab482
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/index.ts
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+import cluster from './components/page-configure-advanced-cluster';
+import models from './components/page-configure-advanced-models';
+import caches from './components/page-configure-advanced-caches';
+import igfs from './components/page-configure-advanced-igfs';
+import cacheEditForm from './components/cache-edit-form';
+import clusterEditForm from './components/cluster-edit-form';
+import igfsEditForm from './components/igfs-edit-form';
+import modelEditForm from './components/model-edit-form';
+
+export default angular
+    .module('ignite-console.page-configure-advanced', [
+        cluster.name,
+        models.name,
+        caches.name,
+        igfs.name,
+        igfsEditForm.name,
+        modelEditForm.name,
+        cacheEditForm.name,
+        clusterEditForm.name
+    ])
+    .component('pageConfigureAdvanced', component);
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/style.scss b/modules/frontend/app/configuration/components/page-configure-advanced/style.scss
new file mode 100644
index 0000000..9519682
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/style.scss
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+page-configure-advanced {
+    @import "public/stylesheets/variables.scss";
+
+    $nav-height: 46px;
+
+    display: flex;
+    flex-direction: column;
+
+    .pca-form-blocked {
+        opacity: 0.5;
+        pointer-events: none;
+        transition: opacity 0.2s;
+    }
+
+    .pca-menu {
+        height: $nav-height;
+        box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
+    }
+
+    .pca-menu > ul {
+        margin: 0;
+        padding: 0;
+        display: flex;
+        flex-direction: row;
+        height: $nav-height;
+        background-color: #f9f9f9;
+        list-style: none;
+        border-bottom: 1px solid #dddddd !important;
+        position: -webkit-sticky;
+        position: sticky;
+
+        .pca-menu-link {
+            border-radius: 0;
+            color: #393939;
+            font-weight: normal;
+            font-size: 12px;
+            padding: 15px 20px 14px;
+            line-height: 16px;
+            border-right: 1px solid #dddddd;
+            text-decoration: none !important;
+            transition-duration: 0.2s;
+            transition-property: border-bottom, background-color, color, padding-bottom;
+            border-bottom: $ignite-brand-primary 0px solid;
+
+            &.active, &:hover {
+                background-color: white;
+                color: $ignite-brand-primary;
+                border-bottom: $ignite-brand-primary 3px solid;
+                padding-bottom: 12px;
+            }
+
+            &:hover:not(.active) {
+                border-bottom-color: lighten($ignite-brand-primary, 25%)
+            }
+        }
+    }
+
+    panel-collapsible {
+        margin-bottom: 20px;
+    }
+
+    panel-content.pca-form-row {
+        display: flex !important;
+    }
+
+    .pca-form-row {
+        display: flex;
+        flex-direction: row;
+
+        .pca-form-column-6 {
+            flex: 6;
+        }
+    }
+
+    // Aligns config section form and preview top
+    .pca-form-column-6.pc-form-grid-row {
+        margin-top: -10px;
+    }
+}
diff --git a/modules/frontend/app/configuration/components/page-configure-advanced/template.pug b/modules/frontend/app/configuration/components/page-configure-advanced/template.pug
new file mode 100644
index 0000000..ba594f3
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-advanced/template.pug
@@ -0,0 +1,24 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+nav.pca-menu
+    ul
+        li(ng-repeat='item in $ctrl.menuItems')
+            a.pca-menu-link.btn-ignite(ui-sref-active='active' ui-sref='{{::item.sref}}')
+                svg(ng-if='::item.icon' ignite-icon='{{::item.icon}}').icon-left
+                |{{::item.text}}
+
+ui-view
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-basic/component.ts b/modules/frontend/app/configuration/components/page-configure-basic/component.ts
new file mode 100644
index 0000000..bb2f7f7
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-basic/component.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+import controller from './controller';
+
+export default {
+    template,
+    controller
+};
diff --git a/modules/frontend/app/configuration/components/page-configure-basic/controller.ts b/modules/frontend/app/configuration/components/page-configure-basic/controller.ts
new file mode 100644
index 0000000..6bce731
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-basic/controller.ts
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {forkJoin, merge} from 'rxjs';
+import {distinctUntilChanged, filter, map, pluck, publishReplay, refCount, switchMap, take, tap} from 'rxjs/operators';
+import cloneDeep from 'lodash/cloneDeep';
+import get from 'lodash/get';
+import naturalCompare from 'natural-compare-lite';
+import {basicSave, basicSaveAndDownload, changeItem, removeClusterItems} from '../../store/actionCreators';
+
+import {Confirm} from 'app/services/Confirm.service';
+import ConfigureState from '../../services/ConfigureState';
+import ConfigSelectors from '../../store/selectors';
+import Caches from '../../services/Caches';
+import Clusters from '../../services/Clusters';
+import IgniteVersion from 'app/services/Version.service';
+import {default as ConfigChangesGuard} from '../../services/ConfigChangesGuard';
+import {UIRouter} from '@uirouter/angularjs';
+import FormUtils from 'app/services/FormUtils.service';
+
+export default class PageConfigureBasicController {
+    form: ng.IFormController;
+
+    static $inject = [
+        'Confirm', '$uiRouter', 'ConfigureState', 'ConfigSelectors', 'Clusters', 'Caches', 'IgniteVersion', '$element', 'ConfigChangesGuard', 'IgniteFormUtils', '$scope'
+    ];
+
+    constructor(
+        private Confirm: Confirm,
+        private $uiRouter: UIRouter,
+        private ConfigureState: ConfigureState,
+        private ConfigSelectors: ConfigSelectors,
+        private Clusters: Clusters,
+        private Caches: Caches,
+        private IgniteVersion: IgniteVersion,
+        private $element: JQLite,
+        private ConfigChangesGuard: ReturnType<typeof ConfigChangesGuard>,
+        private IgniteFormUtils: ReturnType<typeof FormUtils>,
+        private $scope: ng.IScope
+    ) {}
+
+    $onDestroy() {
+        this.subscription.unsubscribe();
+        if (this.onBeforeTransition) this.onBeforeTransition();
+        this.$element = null;
+    }
+
+    $postLink() {
+        this.$element.addClass('panel--ignite');
+    }
+
+    _uiCanExit($transition$) {
+        const options = $transition$.options();
+
+        if (options.custom.justIDUpdate || options.redirectedFrom)
+            return true;
+
+        $transition$.onSuccess({}, () => this.reset());
+
+        return forkJoin(
+            this.ConfigureState.state$.pipe(pluck('edit', 'changes'), take(1)),
+            this.clusterID$.pipe(
+                switchMap((id) => this.ConfigureState.state$.pipe(this.ConfigSelectors.selectClusterShortCaches(id))),
+                take(1)
+            ),
+            this.shortCaches$.pipe(take(1))
+        )
+            .toPromise()
+            .then(([changes, originalShortCaches, currentCaches]) => {
+                return this.ConfigChangesGuard.guard(
+                    {
+                        cluster: this.Clusters.normalize(this.originalCluster),
+                        caches: originalShortCaches.map(this.Caches.normalize)
+                    },
+                    {
+                        cluster: {...this.Clusters.normalize(this.clonedCluster), caches: changes.caches.ids},
+                        caches: currentCaches.map(this.Caches.normalize)
+                    }
+                );
+            });
+    }
+
+    $onInit() {
+        this.onBeforeTransition = this.$uiRouter.transitionService.onBefore({}, (t) => this._uiCanExit(t));
+
+        this.memorySizeInputVisible$ = this.IgniteVersion.currentSbj.pipe(
+            map((version) => this.IgniteVersion.since(version.ignite, '2.0.0'))
+        );
+
+        const clusterID$ = this.$uiRouter.globals.params$.pipe(
+            take(1),
+            pluck('clusterID'),
+            filter((v) => v),
+            take(1)
+        );
+        this.clusterID$ = clusterID$;
+
+        this.isNew$ = this.$uiRouter.globals.params$.pipe(pluck('clusterID'), map((id) => id === 'new'));
+        this.shortCaches$ = this.ConfigureState.state$.pipe(this.ConfigSelectors.selectCurrentShortCaches);
+        this.shortClusters$ = this.ConfigureState.state$.pipe(this.ConfigSelectors.selectShortClustersValue());
+        this.originalCluster$ = clusterID$.pipe(
+            distinctUntilChanged(),
+            switchMap((id) => {
+                return this.ConfigureState.state$.pipe(this.ConfigSelectors.selectClusterToEdit(id));
+            }),
+            distinctUntilChanged(),
+            publishReplay(1),
+            refCount()
+        );
+
+        this.subscription = merge(
+            this.shortCaches$.pipe(
+                map((caches) => caches.sort((a, b) => naturalCompare(a.name, b.name))),
+                tap((v) => this.shortCaches = v)
+            ),
+            this.shortClusters$.pipe(tap((v) => this.shortClusters = v)),
+            this.originalCluster$.pipe(tap((v) => {
+                this.originalCluster = v;
+                // clonedCluster should be set only when particular cluster edit starts.
+                // 
+                // Stored cluster changes should not propagate to clonedCluster because it's assumed
+                // that last saved copy has same shape to what's already loaded. If stored cluster would overwrite
+                // clonedCluster every time, then data rollback on server errors would undo all changes
+                // made by user and we don't want that. Advanced configuration forms do the same too.
+                if (get(v, '_id') !== get(this.clonedCluster, '_id')) this.clonedCluster = cloneDeep(v);
+                this.defaultMemoryPolicy = this.Clusters.getDefaultClusterMemoryPolicy(this.clonedCluster);
+            }))
+        ).subscribe();
+
+        this.formActionsMenu = [
+            {
+                text: 'Save and Download',
+                click: () => this.save(true),
+                icon: 'download'
+            },
+            {
+                text: 'Save',
+                click: () => this.save(),
+                icon: 'checkmark'
+            }
+        ];
+
+        this.cachesColDefs = [
+            {name: 'Name:', cellClass: 'pc-form-grid-col-10'},
+            {name: 'Mode:', cellClass: 'pc-form-grid-col-10'},
+            {name: 'Atomicity:', cellClass: 'pc-form-grid-col-20', tip: `
+                Atomicity:
+                <ul>
+                    <li>ATOMIC - in this mode distributed transactions and distributed locking are not supported</li>
+                    <li>TRANSACTIONAL - in this mode specified fully ACID-compliant transactional cache behavior</li>
+                    <li>TRANSACTIONAL_SNAPSHOT - in this mode specified fully ACID-compliant transactional cache behavior for both key-value API and SQL transactions</li>
+                </ul>
+            `},
+            {name: 'Backups:', cellClass: 'pc-form-grid-col-10', tip: `
+                Number of nodes used to back up single partition for partitioned cache
+            `}
+        ];
+    }
+
+    addCache() {
+        this.ConfigureState.dispatchAction({type: 'ADD_CACHE_TO_EDIT'});
+    }
+
+    removeCache(cache) {
+        this.ConfigureState.dispatchAction(
+            removeClusterItems(this.$uiRouter.globals.params.clusterID, 'caches', [cache._id], false, false)
+        );
+    }
+
+    changeCache(cache) {
+        return this.ConfigureState.dispatchAction(changeItem('caches', cache));
+    }
+
+    save(download = false) {
+        if (this.form.$invalid)
+            return this.IgniteFormUtils.triggerValidation(this.form, this.$scope);
+
+        this.ConfigureState.dispatchAction((download ? basicSaveAndDownload : basicSave)(cloneDeep(this.clonedCluster)));
+    }
+
+    reset() {
+        this.clonedCluster = cloneDeep(this.originalCluster);
+        this.ConfigureState.dispatchAction({type: 'RESET_EDIT_CHANGES'});
+    }
+
+    confirmAndReset() {
+        return this.Confirm.confirm('Are you sure you want to undo all changes for current cluster?')
+            .then(() => this.reset())
+            .catch(() => {});
+    }
+}
diff --git a/modules/frontend/app/configuration/components/page-configure-basic/index.ts b/modules/frontend/app/configuration/components/page-configure-basic/index.ts
new file mode 100644
index 0000000..a7bd402
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-basic/index.ts
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import component from './component';
+import {reducer} from './reducer';
+
+export default angular
+    .module('ignite-console.page-configure-basic', [])
+    .run(['ConfigureState', (ConfigureState) => ConfigureState.addReducer((state, action) => Object.assign(state, {
+        configureBasic: reducer(state.configureBasic, action, state)
+    }))])
+    .component('pageConfigureBasic', component);
diff --git a/modules/frontend/app/configuration/components/page-configure-basic/reducer.spec.js b/modules/frontend/app/configuration/components/page-configure-basic/reducer.spec.js
new file mode 100644
index 0000000..6383e61
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-basic/reducer.spec.js
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {suite, test} from 'mocha';
+import {assert} from 'chai';
+
+import {ADD_NEW_CACHE, reducer, REMOVE_CACHE, SET_CLUSTER, SET_SELECTED_CACHES} from './reducer';
+
+suite('page-configure-basic component reducer', () => {
+    test('Default state', () => {
+        assert.deepEqual(reducer(void 0, {}), {
+            clusterID: -1,
+            cluster: null,
+            newClusterCaches: [],
+            oldClusterCaches: []
+        });
+    });
+
+    test('SET_CLUSTER action', () => {
+        const root = {
+            list: {
+                clusters: new Map([[1, {name: 'New cluster', _id: 1, caches: [1]}]]),
+                caches: new Map([[1, {}]]),
+                spaces: new Map([[0, {}]])
+            }
+        };
+
+        const defaultCluster = {
+            _id: null,
+            discovery: {
+                kind: 'Multicast',
+                Vm: {addresses: ['127.0.0.1:47500..47510']},
+                Multicast: {addresses: ['127.0.0.1:47500..47510']},
+                Jdbc: {initSchema: true},
+                Cloud: {regions: [], zones: []}
+            },
+            space: null,
+            name: null,
+            memoryConfiguration: {
+                memoryPolicies: [{
+                    name: 'default',
+                    maxSize: null
+                }]
+            },
+            caches: []
+        };
+
+        assert.deepEqual(
+            reducer(void 0, {type: SET_CLUSTER, _id: -1, cluster: defaultCluster}, root),
+            {
+                clusterID: -1,
+                cluster: Object.assign({}, defaultCluster, {
+                    _id: -1,
+                    name: 'New cluster (1)',
+                    space: 0
+                }),
+                newClusterCaches: [],
+                oldClusterCaches: []
+            },
+            'inits new cluster if _id is fake'
+        );
+
+        assert.deepEqual(
+            reducer(void 0, {type: SET_CLUSTER, _id: 1}, root),
+            {
+                clusterID: 1,
+                cluster: root.list.clusters.get(1),
+                newClusterCaches: [],
+                oldClusterCaches: [root.list.caches.get(1)]
+            },
+            'inits new cluster if _id is real'
+        );
+    });
+
+    test('ADD_NEW_CACHE action', () => {
+        const state = {
+            clusterID: -1,
+            cluster: {},
+            newClusterCaches: [{name: 'New cache (1)'}],
+            oldClusterCaches: []
+        };
+
+        const root = {
+            list: {
+                caches: new Map([[1, {name: 'New cache'}]]),
+                spaces: new Map([[1, {}]])
+            }
+        };
+
+        const defaultCache = {
+            _id: null,
+            space: null,
+            name: null,
+            cacheMode: 'PARTITIONED',
+            atomicityMode: 'ATOMIC',
+            readFromBackup: true,
+            copyOnRead: true,
+            clusters: [],
+            domains: [],
+            cacheStoreFactory: {CacheJdbcBlobStoreFactory: {connectVia: 'DataSource'}},
+            memoryPolicyName: 'default'
+        };
+
+        assert.deepEqual(
+            reducer(state, {type: ADD_NEW_CACHE, _id: -1}, root),
+            {
+                clusterID: -1,
+                cluster: {},
+                newClusterCaches: [
+                    {name: 'New cache (1)'},
+                    Object.assign({}, defaultCache, {
+                        _id: -1,
+                        space: 1,
+                        name: 'New cache (2)'
+                    })
+                ],
+                oldClusterCaches: []
+            },
+            'adds new cache'
+        );
+    });
+
+    test('REMOVE_CACHE action', () => {
+        const state = {
+            newClusterCaches: [{_id: -1}],
+            oldClusterCaches: [{_id: 1}]
+        };
+
+        assert.deepEqual(
+            reducer(state, {type: REMOVE_CACHE, cache: {_id: null}}),
+            state,
+            'removes nothing if there\'s no matching cache'
+        );
+
+        assert.deepEqual(
+            reducer(state, {type: REMOVE_CACHE, cache: {_id: -1}}),
+            {
+                newClusterCaches: [],
+                oldClusterCaches: [{_id: 1}]
+            },
+            'removes new cluster cache'
+        );
+
+        assert.deepEqual(
+            reducer(state, {type: REMOVE_CACHE, cache: {_id: 1}}),
+            {
+                newClusterCaches: [{_id: -1}],
+                oldClusterCaches: []
+            },
+            'removes old cluster cache'
+        );
+    });
+
+    test('SET_SELECTED_CACHES action', () => {
+        const state = {
+            cluster: {caches: []},
+            oldClusterCaches: []
+        };
+
+        const root = {
+            list: {caches: new Map([[1, {_id: 1}], [2, {_id: 2}], [3, {_id: 3}]])}
+        };
+
+        assert.deepEqual(
+            reducer(state, {type: SET_SELECTED_CACHES, cacheIDs: []}, root),
+            state,
+            'select no caches if action.cacheIDs is empty'
+        );
+
+        assert.deepEqual(
+            reducer(state, {type: SET_SELECTED_CACHES, cacheIDs: [1]}, root),
+            {
+                cluster: {caches: [1]},
+                oldClusterCaches: [{_id: 1}]
+            },
+            'selects existing cache'
+        );
+
+        assert.deepEqual(
+            reducer(state, {type: SET_SELECTED_CACHES, cacheIDs: [1, 2, 3]}, root),
+            {
+                cluster: {caches: [1, 2, 3]},
+                oldClusterCaches: [{_id: 1}, {_id: 2}, {_id: 3}]
+            },
+            'selects three existing caches'
+        );
+    });
+});
diff --git a/modules/frontend/app/configuration/components/page-configure-basic/reducer.ts b/modules/frontend/app/configuration/components/page-configure-basic/reducer.ts
new file mode 100644
index 0000000..ee978a2
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-basic/reducer.ts
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import cloneDeep from 'lodash/cloneDeep';
+import {uniqueName} from 'app/utils/uniqueName';
+
+export const ADD_NEW_CACHE = Symbol('ADD_NEW_CACHE');
+export const REMOVE_CACHE = Symbol('REMOVE_CACHE');
+export const SET_SELECTED_CACHES = Symbol('SET_SELECTED_CACHES');
+export const SET_CLUSTER = Symbol('SET_CLUSTER');
+
+const defaults = {
+    clusterID: -1,
+    cluster: null,
+    newClusterCaches: [],
+    oldClusterCaches: []
+};
+
+const defaultSpace = (root) => [...root.list.spaces.keys()][0];
+const existingCaches = (caches, cluster) => {
+    return cluster.caches.map((id) => {
+        return cloneDeep(caches.get(id));
+    }).filter((v) => v);
+};
+export const isNewItem = (item) => item && item._id < 0;
+
+export const reducer = (state = defaults, action, root) => {
+    switch (action.type) {
+        case SET_CLUSTER: {
+            const cluster = !isNewItem(action)
+                ? cloneDeep(root.list.clusters.get(action._id))
+                : Object.assign({}, action.cluster, {
+                    _id: -1,
+                    space: defaultSpace(root),
+                    name: uniqueName('New cluster', [...root.list.clusters.values()], ({name, i}) => `${name} (${i})`)
+                });
+
+            return Object.assign({}, state, {
+                clusterID: cluster._id,
+                cluster,
+                newClusterCaches: [],
+                oldClusterCaches: existingCaches(root.list.caches, cluster)
+            });
+        }
+
+        case ADD_NEW_CACHE: {
+            const cache = {
+                _id: action._id,
+                space: defaultSpace(root),
+                name: uniqueName('New cache', [...root.list.caches.values(), ...state.newClusterCaches], ({name, i}) => `${name} (${i})`),
+                cacheMode: 'PARTITIONED',
+                atomicityMode: 'ATOMIC',
+                readFromBackup: true,
+                copyOnRead: true,
+                clusters: [],
+                domains: [],
+                cacheStoreFactory: {CacheJdbcBlobStoreFactory: {connectVia: 'DataSource'}},
+                memoryPolicyName: 'default'
+            };
+
+            return Object.assign({}, state, {
+                newClusterCaches: [...state.newClusterCaches, cache]
+            });
+        }
+
+        case REMOVE_CACHE: {
+            const cache = action.cache;
+
+            return Object.assign({}, state, {
+                newClusterCaches: isNewItem(cache)
+                    ? state.newClusterCaches.filter((c) => c._id !== cache._id)
+                    : state.newClusterCaches,
+                oldClusterCaches: isNewItem(cache)
+                    ? state.oldClusterCaches
+                    : state.oldClusterCaches.filter((c) => c._id !== cache._id)
+            });
+        }
+
+        case SET_SELECTED_CACHES: {
+            const value = Object.assign({}, state, {
+                cluster: Object.assign({}, state.cluster, {
+                    caches: [...action.cacheIDs.filter((id) => id)]
+                })
+            });
+
+            value.oldClusterCaches = existingCaches(root.list.caches, value.cluster);
+
+            return value;
+        }
+
+        default:
+            return state;
+    }
+};
diff --git a/modules/frontend/app/configuration/components/page-configure-basic/style.scss b/modules/frontend/app/configuration/components/page-configure-basic/style.scss
new file mode 100644
index 0000000..4814aa4
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-basic/style.scss
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+page-configure-basic {
+    display: block;
+    padding: 30px 20px;
+    $max-row-width: 500px;
+    $row-height: 28px;
+
+    .pcb-row-no-margin {
+        [class*='grid-col'] {
+            margin-top: 0 !important;
+        }
+    }
+
+    .pcb-inner-padding {
+        padding-left: 10px;
+        padding-right: 10px;
+    }
+
+    .pcb-cache-name-row {
+        $margin: 10px;
+
+        display: flex;
+        flex-direction: row;
+        max-width: 100%;
+
+        .ignite-form-field {
+            margin-right: $margin;
+            max-width: $max-row-width;
+            flex-grow: 10;
+        }
+    }
+
+    .pcb-section-notification {
+        font-size: 14px;
+        color: #757575;
+        margin-bottom: 1em;
+    }
+
+    .pcb-section-header {
+        margin-top: 0;
+        margin-bottom: 7px;
+        font-size: 16px;
+        line-height: 19px;
+    }
+
+    .pcb-form-main-buttons {
+        display: flex;
+        flex-direction: row;
+        .pcb-form-main-buttons-left {
+            margin-right: auto;
+        }
+        .pcb-form-main-buttons-right {
+            margin-left: auto;
+        }
+    }
+    .pc-form-actions-panel {
+        margin: 20px -20px -30px;
+        box-shadow: 0px -2px 4px -1px rgba(0, 0, 0, 0.2);
+    }
+
+    .form-field__checkbox {
+        margin-top: auto;
+        margin-bottom: 8px;
+    }
+
+    .pcb-form-grid-row {
+        @media(min-width: 992px) {
+            &>.pc-form-grid-col-10 {
+                flex: 0 0 calc(100% / 6);
+            }
+
+            &>.pc-form-grid-col-20 {
+                flex: 0 0 calc(100% / 6);
+            }
+
+            &>.pc-form-grid-col-30 {
+                flex: 0 0 calc(100% / 4);
+            }
+
+            &>.pc-form-grid-col-40 {
+                flex: 0 0 calc(100% / 3);
+            }
+
+            &>.pc-form-grid-col-60 {
+                flex: 0 0 calc(100% / 2);
+            }
+            &>.pc-form-grid-col-120 {
+                flex: 0 0 100%;
+            }
+            &>.pc-form-grid-col-free {
+                flex: 1;
+            }
+        }
+        @media(max-width: 992px) {
+            &>.pc-form-grid-col-10 {
+                flex: 0 0 calc(100% / 6);
+            }
+
+            &>.pc-form-grid-col-20 {
+                flex: 0 0 calc(100% / 3);
+            }
+
+            &>.pc-form-grid-col-30 {
+                flex: 0 0 calc(100% / 2);
+            }
+
+            &>.pc-form-grid-col-40 {
+                flex: 0 0 calc(100% / 1.5);
+            }
+
+            &>.pc-form-grid-col-60,
+            &>.pc-form-grid-col-120 {
+                flex: 0 0 100%;
+            }
+            &>.pc-form-grid-col-free {
+                flex: 1;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-basic/template.pug b/modules/frontend/app/configuration/components/page-configure-basic/template.pug
new file mode 100644
index 0000000..6852443
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-basic/template.pug
@@ -0,0 +1,198 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+include /app/configuration/mixins
+include ./../page-configure-advanced/components/cluster-edit-form/templates/general/discovery/cloud
+include ./../page-configure-advanced/components/cluster-edit-form/templates/general/discovery/google
+include ./../page-configure-advanced/components/cluster-edit-form/templates/general/discovery/jdbc
+include ./../page-configure-advanced/components/cluster-edit-form/templates/general/discovery/multicast
+include ./../page-configure-advanced/components/cluster-edit-form/templates/general/discovery/s3
+include ./../page-configure-advanced/components/cluster-edit-form/templates/general/discovery/shared
+include ./../page-configure-advanced/components/cluster-edit-form/templates/general/discovery/vm
+include ./../page-configure-advanced/components/cluster-edit-form/templates/general/discovery/zookeeper
+include ./../page-configure-advanced/components/cluster-edit-form/templates/general/discovery/kubernetes
+
+- const model = '$ctrl.clonedCluster'
+- const modelDiscoveryKind = `${model}.discovery.kind`
+- let form = '$ctrl.form'
+
+form(novalidate name=form)
+    h2.pcb-section-header.pcb-inner-padding Step 1. Cluster Configuration
+
+    .pcb-section-notification.pcb-inner-padding(ng-if='!$ctrl.shortClusters')
+        | You have no clusters.
+        | Let’s configure your first and associate it with caches.
+    .pcb-section-notification.pcb-inner-padding(ng-if='$ctrl.shortClusters')
+        | Configure cluster properties and associate your cluster with caches.
+
+    .pc-form-grid-row.pcb-form-grid-row
+        .pc-form-grid-col-60
+            +form-field__text({
+                label: 'Name:',
+                model: `${model}.name`,
+                name: '"clusterName"',
+                placeholder: 'Input name',
+                required: true,
+                tip: 'Instance name allows to indicate to what grid this particular grid instance belongs to'
+            })(
+                ignite-unique='$ctrl.shortClusters'
+                ignite-unique-property='name'
+                ignite-unique-skip=`["_id", ${model}]`
+            )
+                +form-field__error({ error: 'igniteUnique', message: 'Cluster name should be unique.' })
+
+        .pc-form-grid__break
+        .pc-form-grid-col-60
+            +form-field__dropdown({
+                label: 'Discovery:',
+                model: modelDiscoveryKind,
+                name: '"discovery"',
+                placeholder: 'Choose discovery',
+                options: '$ctrl.Clusters.discoveries',
+                tip: 'Discovery allows to discover remote nodes in grid\
+                <ul>\
+                    <li>Static IPs - IP Finder which works only with pre configured list of IP addresses specified</li>\
+                    <li>Multicast - Multicast based IP finder</li>\
+                    <li>AWS S3 - AWS S3 based IP finder that automatically discover cluster nodes on Amazon EC2 cloud</li>\
+                    <li>Apache jclouds - Apache jclouds multi cloud toolkit based IP finder for cloud platforms with unstable IP addresses</li>\
+                    <li>Google cloud storage - Google Cloud Storage based IP finder that automatically discover cluster nodes on Google Compute Engine cluster</li>\
+                    <li>JDBC - JDBC based IP finder that use database to store node IP address</li>\
+                    <li>Shared filesystem - Shared filesystem based IP finder that use file to store node IP address</li>\
+                    <li>Apache ZooKeeper - Apache ZooKeeper based IP finder when you use ZooKeeper to coordinate your distributed environment</li>\
+                    <li>Kubernetes - IP finder for automatic lookup of Ignite nodes running in Kubernetes environment</li>\
+                </ul>'
+            })
+        .pc-form-grid__break
+        .pc-form-group
+            +discovery-vm(model)(class='pcb-form-grid-row' ng-if=`${modelDiscoveryKind} === 'Vm'`)
+            +discovery-cloud(model)(class='pcb-form-grid-row' ng-if=`${modelDiscoveryKind} === 'Cloud'`)
+            +discovery-google(model)(class='pcb-form-grid-row' ng-if=`${modelDiscoveryKind} === 'GoogleStorage'`)
+            +discovery-jdbc(model)(class='pcb-form-grid-row' ng-if=`${modelDiscoveryKind} === 'Jdbc'`)
+            +discovery-multicast(model)(class='pcb-form-grid-row' ng-if=`${modelDiscoveryKind} === 'Multicast'`)
+            +discovery-s3(model)(class='pcb-form-grid-row' ng-if=`${modelDiscoveryKind} === 'S3'`)
+            +discovery-shared(model)(class='pcb-form-grid-row' ng-if=`${modelDiscoveryKind} === 'SharedFs'`)
+            +discovery-zookeeper(model)(class='pcb-form-grid-row' ng-if=`${modelDiscoveryKind} === 'ZooKeeper'`)
+            +discovery-kubernetes(model)(class='pcb-form-grid-row' ng-if=`${modelDiscoveryKind} === 'Kubernetes'`)
+
+    h2.pcb-section-header.pcb-inner-padding(style='margin-top:30px') Step 2. Caches Configuration
+
+    .pcb-form-grid-row.pc-form-grid-row
+        .pc-form-grid-col-60(
+            ng-if=`
+                $ctrl.defaultMemoryPolicy &&
+                $ctrl.IgniteVersion.available(['2.0.0', '2.3.0']) &&
+                $ctrl.memorySizeInputVisible$|async:this
+            `
+        )
+            form-field-size(
+                ng-model='$ctrl.defaultMemoryPolicy.maxSize'
+                ng-model-options='{allowInvalid: true}'
+                id='memory'
+                name='memory'
+                label='Total Off-heap Size:'
+                size-type='bytes'
+                size-scale-label='mb'
+                placeholder='{{ ::$ctrl.Clusters.memoryPolicy.maxSize.default }}'
+                min='{{ ::$ctrl.Clusters.memoryPolicy.maxSize.min($ctrl.defaultMemoryPolicy) }}'
+                tip='“default” cluster memory policy off-heap max memory size. Leave empty to use 80% of physical memory available on current machine. Should be at least 10Mb.'
+                on-scale-change='scale = $event'
+            )
+                +form-field__error({ error: 'min', message: 'Maximum size should be equal to or more than initial size ({{ $ctrl.Clusters.memoryPolicy.maxSize.min($ctrl.defaultMemoryPolicy) / scale.value}} {{scale.label}}).' })
+
+        .pc-form-grid-col-60(ng-if=`$ctrl.IgniteVersion.available('2.3.0')`)
+            form-field-size(
+                ng-model=`${model}.dataStorageConfiguration.defaultDataRegionConfiguration.maxSize`
+                ng-model-options='{allowInvalid: true}'
+                id='memory'
+                name='memory'
+                label='Total Off-heap Size:'
+                size-type='bytes'
+                size-scale-label='mb'
+                placeholder='{{ ::$ctrl.Clusters.dataRegion.maxSize.default }}'
+                min=`{{ ::$ctrl.Clusters.dataRegion.maxSize.min(${model}.dataStorageConfiguration.defaultDataRegionConfiguration) }}`
+                tip='Default data region off-heap max memory size. Leave empty to use 20% of physical memory available on current machine. Should be at least 10Mb.'
+                on-scale-change='scale = $event'
+            )
+                +form-field__error({ error: 'min', message: `Maximum size should be equal to or more than initial size ({{ $ctrl.Clusters.dataRegion.maxSize.min(${model}.dataStorageConfiguration.defaultDataRegionConfiguration) / scale.value}} {{scale.label}}).` })
+        .pc-form-grid-col-120
+            .ignite-form-field
+                list-editable.pcb-caches-list(
+                    ng-model='$ctrl.shortCaches'
+                    list-editable-one-way
+                    on-item-change='$ctrl.changeCache($event)'
+                    on-item-remove='$ctrl.removeCache($event)'
+                    list-editable-cols='::$ctrl.cachesColDefs'
+                    list-editable-cols-row-class='pc-form-grid-row pcb-row-no-margin'
+                )
+                    list-editable-item-view
+                        div {{ $item.name }}
+                        div {{ $item.cacheMode }}
+                        div {{ $item.atomicityMode }}
+                        div {{ $ctrl.Caches.getCacheBackupsCount($item) }}
+                    list-editable-item-edit
+                        div
+                            +form-field__text({
+                                label: 'Name',
+                                model: '$item.name',
+                                name: '"name"',
+                                required: true
+                            })(
+                                ignite-unique='$ctrl.shortCaches'
+                                ignite-unique-property='name'
+                                ignite-form-field-input-autofocus='true'
+                            )
+                                +form-field__error({ error: 'igniteUnique', message: 'Cache name should be unqiue' })
+                        div
+                            +form-field__cache-modes({
+                                label: 'Mode:',
+                                model: '$item.cacheMode',
+                                name: '"cacheMode"',
+                                placeholder: 'PARTITIONED'
+                            })
+                        div
+                            +form-field__dropdown({
+                                label: 'Atomicity:',
+                                model: '$item.atomicityMode',
+                                name: '"atomicityMode"',
+                                placeholder: 'ATOMIC',
+                                options: '::$ctrl.Caches.atomicityModes'
+                            })
+                        div(ng-show='$ctrl.Caches.shouldShowCacheBackupsCount($item)')
+                            +form-field__number({
+                                label: 'Backups:',
+                                model: '$item.backups',
+                                name: '"backups"',
+                                placeholder: '0',
+                                min: 0
+                            })
+                    list-editable-no-items
+                        list-editable-add-item-button(
+                            add-item='$ctrl.addCache()'
+                            label-single='cache'
+                            label-multiple='caches'
+                        )
+
+    .pc-form-actions-panel
+        button-preview-project(ng-hide='$ctrl.isNew$|async:this' cluster=model)
+
+        .pc-form-actions-panel__right-after
+        button.btn-ignite.btn-ignite--link-success(
+            type='button'
+            ng-click='$ctrl.confirmAndReset()'
+        )
+            | Cancel
+        pc-split-button(actions=`::$ctrl.formActionsMenu`)
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-overview/component.ts b/modules/frontend/app/configuration/components/page-configure-overview/component.ts
new file mode 100644
index 0000000..bb2f7f7
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-overview/component.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+import controller from './controller';
+
+export default {
+    template,
+    controller
+};
diff --git a/modules/frontend/app/configuration/components/page-configure-overview/components/pco-grid-column-categories/directive.ts b/modules/frontend/app/configuration/components/page-configure-overview/components/pco-grid-column-categories/directive.ts
new file mode 100644
index 0000000..4db0791
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-overview/components/pco-grid-column-categories/directive.ts
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import isEqual from 'lodash/isEqual';
+import map from 'lodash/map';
+import uniqBy from 'lodash/uniqBy';
+import headerTemplate from 'app/primitives/ui-grid-header/index.tpl.pug';
+import {IGridColumn, IUiGridConstants} from 'ui-grid';
+
+const visibilityChanged = (a, b) => {
+    return !isEqual(map(a, 'visible'), map(b, 'visible'));
+};
+
+const notSelectionColumn = (cc: IGridColumn): boolean => cc.colDef.name !== 'selectionRowHeaderCol';
+
+/**
+ * Generates categories for uiGrid columns
+ */
+export default function directive(uiGridConstants: IUiGridConstants) {
+    return {
+        require: '^uiGrid',
+        link: {
+            pre(scope, el, attr, grid) {
+                if (!grid.grid.options.enableColumnCategories)
+                    return;
+
+                grid.grid.api.core.registerColumnsProcessor((cp) => {
+                    const oldCategories = grid.grid.options.categories;
+                    const newCategories = uniqBy(cp.filter(notSelectionColumn).map(({colDef: cd}) => {
+                        cd.categoryDisplayName = cd.categoryDisplayName || cd.displayName;
+                        return {
+                            name: cd.categoryDisplayName || cd.displayName,
+                            enableHiding: cd.enableHiding,
+                            visible: !!cd.visible
+                        };
+                    }), 'name');
+
+                    if (visibilityChanged(oldCategories, newCategories)) {
+                        grid.grid.options.categories = newCategories;
+                        // If you don't call this, grid-column-selector won't apply calculated categories
+                        grid.grid.callDataChangeCallbacks(uiGridConstants.dataChange.COLUMN);
+                    }
+
+                    return cp;
+                });
+                grid.grid.options.headerTemplate = headerTemplate;
+            }
+        }
+    };
+}
+
+directive.$inject = ['uiGridConstants'];
diff --git a/modules/frontend/app/configuration/components/page-configure-overview/controller.ts b/modules/frontend/app/configuration/components/page-configure-overview/controller.ts
new file mode 100644
index 0000000..6d6f4f4
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-overview/controller.ts
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Observable, Subject} from 'rxjs';
+import {map} from 'rxjs/operators';
+import naturalCompare from 'natural-compare-lite';
+import {default as ConfigureState} from '../../services/ConfigureState';
+import {default as ConfigSelectors} from '../../store/selectors';
+import {default as Clusters} from '../../services/Clusters';
+import {default as ModalPreviewProject} from '../../components/modal-preview-project/service';
+import {default as ConfigurationDownload} from '../../services/ConfigurationDownload';
+
+import {confirmClustersRemoval} from '../../store/actionCreators';
+
+import {UIRouter} from '@uirouter/angularjs';
+import {ShortCluster} from '../../types';
+import {IColumnDefOf} from 'ui-grid';
+
+const cellTemplate = (state) => `
+    <div class="ui-grid-cell-contents">
+        <a
+            class="link-success"
+            ui-sref="${state}({clusterID: row.entity._id})"
+            title='Click to edit'
+        >{{ row.entity[col.field] }}</a>
+    </div>
+`;
+
+export default class PageConfigureOverviewController {
+    static $inject = [
+        '$uiRouter',
+        'ModalPreviewProject',
+        'Clusters',
+        'ConfigureState',
+        'ConfigSelectors',
+        'ConfigurationDownload'
+    ];
+
+    constructor(
+        private $uiRouter: UIRouter,
+        private ModalPreviewProject: ModalPreviewProject,
+        private Clusters: Clusters,
+        private ConfigureState: ConfigureState,
+        private ConfigSelectors: ConfigSelectors,
+        private ConfigurationDownload: ConfigurationDownload
+    ) {}
+
+    shortClusters$: Observable<Array<ShortCluster>>;
+    clustersColumnDefs: Array<IColumnDefOf<ShortCluster>>;
+    selectedRows$: Subject<Array<ShortCluster>>;
+    selectedRowsIDs$: Observable<Array<string>>;
+
+    $onDestroy() {
+        this.selectedRows$.complete();
+    }
+
+    removeClusters(clusters: Array<ShortCluster>) {
+        this.ConfigureState.dispatchAction(confirmClustersRemoval(clusters.map((c) => c._id)));
+
+        // TODO: Implement storing selected rows in store to share this data between other components.
+        this.selectedRows$.next([]);
+    }
+
+    editCluster(cluster: ShortCluster) {
+        return this.$uiRouter.stateService.go('^.edit', {clusterID: cluster._id});
+    }
+
+    $onInit() {
+        this.shortClusters$ = this.ConfigureState.state$.pipe(this.ConfigSelectors.selectShortClustersValue());
+
+        this.clustersColumnDefs = [
+            {
+                name: 'name',
+                displayName: 'Name',
+                field: 'name',
+                enableHiding: false,
+                filter: {
+                    placeholder: 'Filter by name…'
+                },
+                sort: {direction: 'asc', priority: 0},
+                sortingAlgorithm: naturalCompare,
+                cellTemplate: cellTemplate('base.configuration.edit'),
+                minWidth: 165
+            },
+            {
+                name: 'discovery',
+                displayName: 'Discovery',
+                field: 'discovery',
+                multiselectFilterOptions: this.Clusters.discoveries,
+                width: 150
+            },
+            {
+                name: 'caches',
+                displayName: 'Caches',
+                field: 'cachesCount',
+                cellClass: 'ui-grid-number-cell',
+                cellTemplate: cellTemplate('base.configuration.edit.advanced.caches'),
+                enableFiltering: false,
+                type: 'number',
+                width: 95
+            },
+            {
+                name: 'models',
+                displayName: 'Models',
+                field: 'modelsCount',
+                cellClass: 'ui-grid-number-cell',
+                cellTemplate: cellTemplate('base.configuration.edit.advanced.models'),
+                enableFiltering: false,
+                type: 'number',
+                width: 95
+            },
+            {
+                name: 'igfs',
+                displayName: 'IGFS',
+                field: 'igfsCount',
+                cellClass: 'ui-grid-number-cell',
+                cellTemplate: cellTemplate('base.configuration.edit.advanced.igfs'),
+                enableFiltering: false,
+                type: 'number',
+                width: 80
+            }
+        ];
+
+        this.selectedRows$ = new Subject();
+
+        this.selectedRowsIDs$ = this.selectedRows$.pipe(map((selectedClusters) => selectedClusters.map((cluster) => cluster._id)));
+
+        this.actions$ = this.selectedRows$.pipe(map((selectedClusters) => [
+            {
+                action: 'Edit',
+                click: () => this.editCluster(selectedClusters[0]),
+                available: selectedClusters.length === 1
+            },
+            {
+                action: 'See project structure',
+                click: () => this.ModalPreviewProject.open(selectedClusters[0]),
+                available: selectedClusters.length === 1
+            },
+            {
+                action: 'Download project',
+                click: () => this.ConfigurationDownload.downloadClusterConfiguration(selectedClusters[0]),
+                available: selectedClusters.length === 1
+            },
+            {
+                action: 'Delete',
+                click: () => this.removeClusters(selectedClusters),
+                available: true
+            }
+        ]));
+    }
+}
diff --git a/modules/frontend/app/configuration/components/page-configure-overview/index.ts b/modules/frontend/app/configuration/components/page-configure-overview/index.ts
new file mode 100644
index 0000000..a69a70e
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-overview/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import component from './component';
+import gridColumnCategories from './components/pco-grid-column-categories/directive';
+
+export default angular
+    .module('ignite-console.page-configure-overview', [])
+    .component('pageConfigureOverview', component)
+    .directive('pcoGridColumnCategories', gridColumnCategories);
diff --git a/modules/frontend/app/configuration/components/page-configure-overview/style.scss b/modules/frontend/app/configuration/components/page-configure-overview/style.scss
new file mode 100644
index 0000000..e461d06
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-overview/style.scss
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+page-configure-overview {
+    .pco-relative-root {
+        position: relative;
+    }
+    .pco-table-context-buttons {
+        position: absolute;
+        right: 0;
+        top: -29px - 36px;
+        display: flex;
+        flex-direction: row;
+
+        &>* {
+            margin-left: 20px;
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/page-configure-overview/template.pug b/modules/frontend/app/configuration/components/page-configure-overview/template.pug
new file mode 100644
index 0000000..be8e999
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure-overview/template.pug
@@ -0,0 +1,44 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+header.header-with-selector
+    div
+        h1 Configuration
+        version-picker
+    div
+        a.btn-ignite.btn-ignite--primary(
+            type='button'
+            ui-sref='^.edit({clusterID: "new"})'
+        )
+            svg.icon-left(ignite-icon='plus')
+            | Create Cluster Configuration
+        button-import-models(cluster-id='::"new"')
+
+.pco-relative-root
+    pc-items-table(
+        table-title='::"My Cluster Configurations"'
+        column-defs='$ctrl.clustersColumnDefs'
+        items='$ctrl.shortClusters$|async:this'
+        on-action='$ctrl.onClustersAction($event)'
+        max-rows-to-show='10'
+        one-way-selection='::false'
+        selected-row-id='$ctrl.selectedRowsIDs$|async:this'
+        on-selection-change='$ctrl.selectedRows$.next($event)'
+        actions-menu='$ctrl.actions$|async:this'
+    )
+        footer-slot(ng-hide='($ctrl.shortClusters$|async:this).length' style='font-style: italic')
+            | You have no cluster configurations.
+            a.link-success(ui-sref='base.configuration.edit.basic({clusterID: "new"})') Create one?
diff --git a/modules/frontend/app/configuration/components/page-configure/component.ts b/modules/frontend/app/configuration/components/page-configure/component.ts
new file mode 100644
index 0000000..f46af11
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure/component.ts
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+import controller from './controller';
+
+export default {
+    template,
+    controller,
+    bindings: {
+        cluster$: '<'
+    }
+};
diff --git a/modules/frontend/app/configuration/components/page-configure/controller.ts b/modules/frontend/app/configuration/components/page-configure/controller.ts
new file mode 100644
index 0000000..d8d55ea
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure/controller.ts
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import get from 'lodash/get';
+import {combineLatest, Observable} from 'rxjs';
+import {map, pluck, switchMap} from 'rxjs/operators';
+import {default as ConfigureState} from '../../services/ConfigureState';
+import {default as ConfigSelectors} from '../../store/selectors';
+import {UIRouter} from '@uirouter/angularjs';
+
+export default class PageConfigureController {
+    static $inject = ['$uiRouter', 'ConfigureState', 'ConfigSelectors'];
+
+    constructor(
+        private $uiRouter: UIRouter,
+        private ConfigureState: ConfigureState,
+        private ConfigSelectors: ConfigSelectors
+    ) {}
+
+    clusterID$: Observable<string>;
+    clusterName$: Observable<string>;
+    tooltipsVisible = true;
+
+    $onInit() {
+        this.clusterID$ = this.$uiRouter.globals.params$.pipe(pluck('clusterID'));
+
+        const cluster$ = this.clusterID$.pipe(switchMap((id) => this.ConfigureState.state$.pipe(this.ConfigSelectors.selectCluster(id))));
+
+        const isNew$ = this.clusterID$.pipe(map((v) => v === 'new'));
+
+        this.clusterName$ = combineLatest(cluster$, isNew$, (cluster, isNew) => {
+            return `${isNew ? 'Create' : 'Edit'} cluster configuration ${isNew ? '' : `‘${get(cluster, 'name')}’`}`;
+        });
+    }
+
+    $onDestroy() {}
+}
diff --git a/modules/frontend/app/configuration/components/page-configure/index.ts b/modules/frontend/app/configuration/components/page-configure/index.ts
new file mode 100644
index 0000000..60ae726
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import component from './component';
+
+export default angular.module('ignite-console.configuration.page-configure', [])
+    .component('pageConfigure', component);
diff --git a/modules/frontend/app/configuration/components/page-configure/style.scss b/modules/frontend/app/configuration/components/page-configure/style.scss
new file mode 100644
index 0000000..335f95b
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure/style.scss
@@ -0,0 +1,334 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+page-configure {
+    flex: 1 0 auto;
+    display: flex;
+    flex-direction: column;
+
+    &>.pc-page-header {
+        display: flex;
+
+        .pc-page-header-title {
+            margin-right: auto;
+        }
+    }
+
+    .pc-form-actions-panel {
+        display: flex;
+        flex-direction: row;
+        padding: 10px 20px 10px 30px;
+        box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 3px 4px -1px rgba(0, 0, 0, 0.2);
+        position: -webkit-sticky;
+        position: sticky;
+        bottom: 0;
+        // margin: 20px -30px -30px;
+        background: white;
+        border-radius: 0 0 4px 4px;
+        z-index: 10;
+
+        &>*+* {
+            margin-left: 10px;
+        }
+
+        .link-primary + .link-primary {
+            margin-left: 40px;
+        }
+
+        .pc-form-actions-panel__right-after {
+            width: 0;
+            margin-left: auto;
+        }
+    }
+
+    .pc-hide-tooltips {
+        .tipField:not(.fa-remove), .icon-help, [ignite-icon='info'] {
+            display: none;
+        }
+    }
+
+    .pc-content-container {
+        position: relative;
+
+        &, &>ui-view {
+            flex: 1 0 auto;
+            display: flex;
+            flex-direction: column;
+        }
+
+        .pc-tooltips-toggle {
+            position: absolute;
+            top: 0;
+            right: 0;
+        }
+
+        &>.tabs {
+            flex: 0 0 auto;
+        }
+    }
+
+    .pc-tooltips-toggle {
+        display: inline-flex;
+        height: 40px;
+        align-items: center;
+        width: auto;
+        max-width: none !important;
+        user-select: none;
+
+        &>*:not(input) {
+            margin-left: 5px;
+            flex: 0 0 auto;
+        }
+
+        input {
+            pointer-events: none;
+        }
+
+        &>div {
+            margin-left: 10px !important;
+        }
+    }
+
+    .form-field__label.required:after {
+        content: '*';
+        margin-left: 0.25em;
+    }
+}
+
+.pc-form-group {
+    $input-height: 36px;
+    width: 100%;
+    border: 1px solid rgba(197, 197, 197, 0.5);
+    border-radius: 4px;
+    padding-bottom: 10px;
+    padding-top: $input-height / 2;
+    margin-top: $input-height / -2;
+
+    &:empty {
+        display: none;
+    }
+
+}
+
+.pc-form-group__text-title {
+    transform: translateY(-9px);
+    --pc-form-group-title-bg-color: white;
+
+    &>span {
+        padding-left: 10px;
+        padding-right: 10px;
+        background: var(--pc-form-group-title-bg-color);
+    }
+
+    &+.pc-form-group {
+        padding-top: 10px;
+    }
+
+    .form-field__checkbox {
+        background-color: white;
+        padding: 0 5px;
+        margin: 0 -5px;
+    }
+}
+
+.pc-form-grid-row > .pc-form-group__text-title[class*='pc-form-grid-col-'] {
+    margin-top: 20px !important;
+}
+
+list-editable .pc-form-group__text-title {
+    --pc-form-group-title-bg-color: var(--le-row-bg-color);
+}
+
+.pc-form-grid-row {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    align-content: flex-start;
+
+    &>[class*='pc-form-grid-col-'] {
+        margin: 10px 0 0 !important;
+        max-width: none;
+        padding: 0 10px;
+        flex-grow: 0;
+        flex-shrink: 0;
+    }
+
+    .group-section {
+        width: 100%;
+    }
+
+    &>.pc-form-grid__break {
+        flex: 1 0 100%;
+    }
+}
+
+// Add this class to list-editable when a pc-form-grid is used inside list-editable-item-edit
+.pc-list-editable-with-form-grid > .le-body > .le-row {
+    & > .le-row-index,
+    & > .le-row-cross {
+        margin-top: 28px;
+    }
+}
+
+// Add this class to list-editable when legacy settings-row classes are used inside list-editable-item-edit
+.pc-list-editable-with-legacy-settings-rows > .le-body > .le-row {
+    & > .le-row-index,
+    & > .le-row-cross {
+        margin-top: 18px;
+    }
+}
+
+.pc-form-grid-row {
+    &>.pc-form-grid-col-10 {
+        flex-basis: calc(100% / 6);
+    }
+
+    &>.pc-form-grid-col-20 {
+        flex-basis: calc(100% / 3);
+    }
+
+    &>.pc-form-grid-col-30 {
+        flex-basis: calc(100% / 2);
+    }
+
+    &>.pc-form-grid-col-40 {
+        flex-basis: calc(100% / 1.5);
+    }
+
+    &>.pc-form-grid-col-60 {
+        flex-basis: calc(100% / 1);
+    }
+
+    &>.pc-form-grid-col-free {
+        flex: 1;
+    }
+
+    @media(max-width: 992px) {
+        &>.pc-form-grid-col-10 {
+            flex-basis: calc(25%);
+        }
+
+        &>.pc-form-grid-col-20 {
+            flex-basis: calc(50%);
+        }
+
+        &>.pc-form-grid-col-30 {
+            flex-basis: calc(100%);
+        }
+
+        &>.pc-form-grid-col-60 {
+            flex-basis: calc(100%);
+        }
+    }
+    // IE11 does not count padding towards flex width
+    @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
+        &>.pc-form-grid-col-10 {
+            flex-basis: calc(99.9% / 6 - 20px);
+        }
+
+        &>.pc-form-grid-col-20 {
+            flex-basis: calc(99.9% / 3 - 20px);
+        }
+
+        &>.pc-form-grid-col-30 {
+            flex-basis: calc(100% / 2 - 20px);
+        }
+
+        &>.pc-form-grid-col-40 {
+            flex-basis: calc(100% / 1.5 - 20px);
+        }
+
+        &>.pc-form-grid-col-60 {
+            flex-basis: calc(100% / 1 - 20px);
+        }
+
+        @media(max-width: 992px) {
+            &>.pc-form-grid-col-20 {
+                flex-basis: calc(50% - 20px);
+            }
+
+            &>.pc-form-grid-col-30 {
+                flex-basis: calc(100% - 20px);
+            }
+
+            &>.pc-form-grid-col-60 {
+                flex-basis: calc(100% - 20px);
+            }
+        }
+    }
+}
+.pc-page-header {
+    font-size: 24px;
+    line-height: 36px;
+    color: #393939;
+    margin: 40px 0 30px;
+    padding: 0;
+    border: none;
+    word-break: break-all;
+
+    .pc-page-header-sub {
+        font-size: 14px;
+        line-height: 20px;
+        color: #757575;
+        margin-left: 8px;
+    }
+
+    version-picker {
+        margin-left: 8px;
+        vertical-align: 2px;
+    }
+
+    button-import-models {
+        flex: 0 0 auto;
+        margin-left: 30px;
+        position: relative;
+        // For some reason button is heigher up by 1px than on overview page
+        top: 1px;
+    }
+}
+
+.pc-form-grid__text-only-item {
+    padding-top: 16px;
+    align-self: flex-start;
+    height: 54px;
+    display: flex;
+    justify-content: center;
+    align-content: center;
+    align-items: center;
+    background: white;
+    z-index: 2;
+}
+
+.config-changes-guard__details {
+    cursor: pointer;
+
+    summary {
+        list-style: none;
+    }
+
+    summary::-webkit-details-marker {
+        display: none;
+    }
+
+    summary:before {
+        content: '▶ ';
+    }
+
+    &[open] summary:before {
+        content: '▼ ';
+    }
+}
diff --git a/modules/frontend/app/configuration/components/page-configure/template.pug b/modules/frontend/app/configuration/components/page-configure/template.pug
new file mode 100644
index 0000000..e161ba5
--- /dev/null
+++ b/modules/frontend/app/configuration/components/page-configure/template.pug
@@ -0,0 +1,49 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+header.header-with-selector
+    div
+        h1 {{ $ctrl.clusterName$|async:this }}
+        version-picker
+    div
+        button-import-models(cluster-id='$ctrl.clusterID$|async:this')
+
+div.pc-content-container
+    ul.tabs.tabs--blue
+        li(role='presentation' ui-sref-active='active')
+            a(ui-sref='base.configuration.edit.basic') Basic
+        li(role='presentation' ui-sref-active='{active: "base.configuration.edit.advanced"}')
+            a(ui-sref='base.configuration.edit.advanced.cluster') Advanced
+
+    label.pc-tooltips-toggle.switcher--ignite
+        svg.icon-left(
+            ignite-icon='info'
+            bs-tooltip=''
+            data-title='Use this setting to hide or show tooltips with hints.'
+            data-placement='left'
+        )
+        span Tooltips
+        input(type='checkbox' ng-model='$ctrl.tooltipsVisible')
+        div
+
+    ui-view.theme--ignite.theme--ignite-errors-horizontal(
+        ignite-loading='configuration'
+        ignite-loading-text='{{ $ctrl.loadingText }}'
+        ignite-loading-position='top'
+        ng-class=`{
+            'pc-hide-tooltips': !$ctrl.tooltipsVisible
+        }`
+    )
diff --git a/modules/frontend/app/configuration/components/pc-items-table/component.js b/modules/frontend/app/configuration/components/pc-items-table/component.js
new file mode 100644
index 0000000..1ca04c0
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pc-items-table/component.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+import controller from './controller';
+
+export default {
+    template,
+    controller,
+    transclude: {
+        footerSlot: '?footerSlot'
+    },
+    bindings: {
+        items: '<',
+        onVisibleRowsChange: '&?',
+        onSortChanged: '&?',
+        onFilterChanged: '&?',
+
+        hideHeader: '<?',
+        rowIdentityKey: '@?',
+
+        columnDefs: '<',
+        tableTitle: '<',
+        selectedRowId: '<?',
+        maxRowsToShow: '@?',
+        onSelectionChange: '&?',
+        oneWaySelection: '<?',
+        incomingActionsMenu: '<?actionsMenu'
+    }
+};
diff --git a/modules/frontend/app/configuration/components/pc-items-table/controller.js b/modules/frontend/app/configuration/components/pc-items-table/controller.js
new file mode 100644
index 0000000..c97e50f
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pc-items-table/controller.js
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import debounce from 'lodash/debounce';
+
+export default class ItemsTableController {
+    static $inject = ['$scope', 'gridUtil', '$timeout', 'uiGridSelectionService'];
+
+    constructor($scope, gridUtil, $timeout, uiGridSelectionService) {
+        Object.assign(this, {$scope, gridUtil, $timeout, uiGridSelectionService});
+        this.rowIdentityKey = '_id';
+    }
+
+    $onInit() {
+        this.grid = {
+            data: this.items,
+            columnDefs: this.columnDefs,
+            rowHeight: 46,
+            enableColumnMenus: false,
+            enableFullRowSelection: true,
+            enableSelectionBatchEvent: true,
+            selectionRowHeaderWidth: 52,
+            enableColumnCategories: true,
+            flatEntityAccess: true,
+            headerRowHeight: 70,
+            modifierKeysToMultiSelect: true,
+            enableFiltering: true,
+            rowIdentity: (row) => {
+                return row[this.rowIdentityKey];
+            },
+            onRegisterApi: (api) => {
+                this.gridAPI = api;
+
+                api.selection.on.rowSelectionChanged(this.$scope, (row, e) => {
+                    this.onRowsSelectionChange([row], e);
+                });
+
+                api.selection.on.rowSelectionChangedBatch(this.$scope, (rows, e) => {
+                    this.onRowsSelectionChange(rows, e);
+                });
+
+                api.core.on.rowsVisibleChanged(this.$scope, () => {
+                    const visibleRows = api.core.getVisibleRows();
+                    if (this.onVisibleRowsChange) this.onVisibleRowsChange({$event: visibleRows});
+                    this.adjustHeight(api, visibleRows.length);
+                    this.showFilterNotification = this.grid.data.length && visibleRows.length === 0;
+                });
+
+                if (this.onFilterChanged) {
+                    api.core.on.filterChanged(this.$scope, () => {
+                        this.onFilterChanged();
+                    });
+                }
+
+                this.$timeout(() => {
+                    if (this.selectedRowId) this.applyIncomingSelection(this.selectedRowId);
+                });
+            },
+            appScopeProvider: this.$scope.$parent
+        };
+        this.actionsMenu = this.makeActionsMenu(this.incomingActionsMenu);
+    }
+
+    oneWaySelection = false;
+
+    onRowsSelectionChange = debounce((rows, e = {}) => {
+        if (e.ignore)
+            return;
+
+        const selected = this.gridAPI.selection.legacyGetSelectedRows();
+
+        if (this.oneWaySelection)
+            rows.forEach((r) => r.isSelected = false);
+
+        if (this.onSelectionChange)
+            this.onSelectionChange({$event: selected});
+    });
+
+    makeActionsMenu(incomingActionsMenu = []) {
+        return incomingActionsMenu;
+    }
+
+    $onChanges(changes) {
+        const hasChanged = (binding) => binding in changes && changes[binding].currentValue !== changes[binding].previousValue;
+
+        if (hasChanged('items') && this.grid) {
+            this.grid.data = changes.items.currentValue;
+            this.gridAPI.grid.modifyRows(this.grid.data);
+            this.adjustHeight(this.gridAPI, this.grid.data.length);
+
+            // Without property existence check non-set selectedRowId binding might cause
+            // unwanted behavior, like unchecking rows during any items change, even if
+            // nothing really changed.
+            if ('selectedRowId' in this)
+                this.applyIncomingSelection(this.selectedRowId);
+        }
+
+        if (hasChanged('selectedRowId') && this.grid && this.grid.data)
+            this.applyIncomingSelection(changes.selectedRowId.currentValue);
+
+        if ('incomingActionsMenu' in changes)
+            this.actionsMenu = this.makeActionsMenu(changes.incomingActionsMenu.currentValue);
+    }
+
+    applyIncomingSelection(selected = []) {
+        this.gridAPI.selection.clearSelectedRows({ignore: true});
+        const rows = this.grid.data.filter((r) => selected.includes(r[this.rowIdentityKey]));
+
+        rows.forEach((r) => {
+            this.gridAPI.selection.selectRow(r, {ignore: true});
+        });
+
+        if (rows.length === 1) {
+            this.$timeout(() => {
+                this.gridAPI.grid.scrollToIfNecessary(this.gridAPI.grid.getRow(rows[0]), null);
+            });
+        }
+    }
+
+    adjustHeight(api, rows) {
+        const maxRowsToShow = this.maxRowsToShow || 5;
+        const headerBorder = 1;
+        const header = this.grid.headerRowHeight + headerBorder;
+        const optionalScroll = (rows ? this.gridUtil.getScrollbarWidth() : 0);
+        const height = Math.min(rows, maxRowsToShow) * this.grid.rowHeight + header + optionalScroll;
+        api.grid.element.css('height', height + 'px');
+        api.core.handleWindowResize();
+    }
+}
diff --git a/modules/frontend/app/configuration/components/pc-items-table/decorator.js b/modules/frontend/app/configuration/components/pc-items-table/decorator.js
new file mode 100644
index 0000000..a63f066
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pc-items-table/decorator.js
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default ['$delegate', 'uiGridSelectionService', ($delegate, uiGridSelectionService) => {
+    $delegate[0].require = ['^uiGrid', '?^pcItemsTable'];
+    $delegate[0].compile = () => ($scope, $el, $attr, [uiGridCtrl, pcItemsTable]) => {
+        const self = uiGridCtrl.grid;
+        $delegate[0].link($scope, $el, $attr, uiGridCtrl);
+        const mySelectButtonClick = (row, evt) => {
+            evt.stopPropagation();
+
+            if (evt.shiftKey)
+                uiGridSelectionService.shiftSelect(self, row, evt, self.options.multiSelect);
+            else
+                uiGridSelectionService.toggleRowSelection(self, row, evt, self.options.multiSelect, self.options.noUnselect);
+        };
+        if (pcItemsTable) $scope.selectButtonClick = mySelectButtonClick;
+    };
+    return $delegate;
+}];
diff --git a/modules/frontend/app/configuration/components/pc-items-table/index.js b/modules/frontend/app/configuration/components/pc-items-table/index.js
new file mode 100644
index 0000000..38f8777
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pc-items-table/index.js
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+import decorator from './decorator';
+
+export default angular
+    .module('ignite-console.page-configure.items-table', ['ui.grid'])
+    .decorator('uiGridSelectionRowHeaderButtonsDirective', decorator)
+    .component('pcItemsTable', component);
diff --git a/modules/frontend/app/configuration/components/pc-items-table/style.scss b/modules/frontend/app/configuration/components/pc-items-table/style.scss
new file mode 100644
index 0000000..7100e72
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pc-items-table/style.scss
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+pc-items-table {
+    @import "public/stylesheets/variables.scss";
+
+    display: block;
+
+    .panel-title {
+        display: flex;
+        flex-direction: row;
+    }
+
+    // Removes unwanted box-shadow and border-right from checkboxes column
+    .ui-grid.ui-grid--ignite .ui-grid-pinned-container.ui-grid-pinned-container-left .ui-grid-render-container-left:before {
+        box-shadow: none;
+    }
+    .ui-grid-pinned-container.ui-grid-pinned-container-left .ui-grid-cell:last-child {
+        border-right: none;
+    }
+    .ui-grid--ignite .ui-grid-header-cell .ui-grid-cell-contents {
+        padding-top: (69px - 20px) / 2;
+        padding-bottom: (69px - 20px) / 2;
+    }
+    footer-slot {
+        $height: 36px + 11px;
+        $line-height: 16px;
+        display: block;
+        line-height: $line-height;
+        font-size: 14px;
+        padding: ($height - $line-height) / 2 20px ($height - $line-height) / 2 70px;
+    }
+    .pco-clusters-table__column-selection {
+        margin-left: 0 !important;
+    }
+    .pco-clusters-table__actions-button {
+        margin-left: auto;
+    }
+    // Fixes header jank
+    .ui-grid-header-viewport {
+        min-height: 70px;
+    }
+    .pc-items-table__selection-count {
+        font-size: 14px;
+        font-style: italic;
+        flex: 0 0 auto;
+    }
+    .pc-items-table__table-name {
+        display: flex;
+    }
+    grid-column-selector {
+        flex: 0 0 auto;
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/pc-items-table/template.pug b/modules/frontend/app/configuration/components/pc-items-table/template.pug
new file mode 100644
index 0000000..75c683f
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pc-items-table/template.pug
@@ -0,0 +1,46 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+.panel--ignite
+    header.header-with-selector(ng-if='!$ctrl.hideHeader')
+        div(ng-hide='$ctrl.gridAPI.selection.getSelectedCount()')
+            span {{ $ctrl.tableTitle }}
+            grid-column-selector(grid-api='$ctrl.gridAPI')
+
+        div(ng-show='$ctrl.gridAPI.selection.getSelectedCount()')
+            grid-item-selected(grid-api='$ctrl.gridAPI')
+
+        div
+            +ignite-form-field-bsdropdown({
+                label: 'Actions',
+                name: 'action',
+                disabled: '!$ctrl.gridAPI.selection.getSelectedCount()',
+                options: '$ctrl.actionsMenu'
+            })
+
+    .grid.ui-grid--ignite(
+        ui-grid='$ctrl.grid'
+        ui-grid-selection
+        pco-grid-column-categories
+        pc-ui-grid-filters
+        ui-grid-resize-columns
+        ui-grid-hovering
+    )
+
+    div(ng-transclude='footerSlot' ng-hide='$ctrl.showFilterNotification')
+    footer-slot(ng-if='$ctrl.showFilterNotification' style='font-style:italic') Nothing to display. Check your filters.
diff --git a/modules/frontend/app/configuration/components/pc-split-button/component.ts b/modules/frontend/app/configuration/components/pc-split-button/component.ts
new file mode 100644
index 0000000..c891f1c
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pc-split-button/component.ts
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+
+export default {
+    controller,
+    template,
+    bindings: {
+        actions: '<'
+    }
+};
diff --git a/modules/frontend/app/configuration/components/pc-split-button/controller.ts b/modules/frontend/app/configuration/components/pc-split-button/controller.ts
new file mode 100644
index 0000000..cee73f8
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pc-split-button/controller.ts
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+type ActionMenuItem = {icon: string, text: string, click: ng.ICompiledExpression};
+type ActionsMenu = ActionMenuItem[];
+
+/**
+ * Groups multiple buttons into a single button with all but first buttons in a dropdown
+ */
+export default class SplitButton {
+    actions: ActionsMenu = [];
+
+    static $inject = ['$element'];
+
+    constructor(private $element: JQLite) {}
+
+    $onInit() {
+        this.$element[0].classList.add('btn-ignite-group');
+    }
+}
diff --git a/modules/frontend/app/configuration/components/pc-split-button/index.ts b/modules/frontend/app/configuration/components/pc-split-button/index.ts
new file mode 100644
index 0000000..4b4b816
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pc-split-button/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('ignite-console.page-configure.pc-split-button', [])
+    .component('pcSplitButton', component);
diff --git a/modules/frontend/app/configuration/components/pc-split-button/template.pug b/modules/frontend/app/configuration/components/pc-split-button/template.pug
new file mode 100644
index 0000000..d44d543
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pc-split-button/template.pug
@@ -0,0 +1,28 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+button.btn-ignite.btn-ignite--success(
+    ng-click='$ctrl.actions[0].click()'
+    type='button'
+)
+    svg(ignite-icon='{{ ::$ctrl.actions[0].icon }}').icon-left
+    | {{ ::$ctrl.actions[0].text }}
+button.btn-ignite.btn-ignite--success(
+    bs-dropdown='$ctrl.actions'
+    data-placement='top-right'
+    type='button'
+)
+    span.icon.fa.fa-caret-down
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/pc-ui-grid-filters/directive.ts b/modules/frontend/app/configuration/components/pc-ui-grid-filters/directive.ts
new file mode 100644
index 0000000..d8330b6
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pc-ui-grid-filters/directive.ts
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+import {IUiGridConstants} from 'ui-grid';
+
+export default function pcUiGridFilters(uiGridConstants: IUiGridConstants) {
+    return {
+        require: 'uiGrid',
+        link: {
+            pre(scope, el, attr, grid) {
+                if (!grid.grid.options.enableFiltering)
+                    return;
+
+                grid.grid.options.columnDefs.filter((cd) => cd.multiselectFilterOptions).forEach((cd) => {
+                    cd.headerCellTemplate = template;
+                    cd.filter = {
+                        type: uiGridConstants.filter.SELECT,
+                        term: cd.multiselectFilterOptions.map((t) => t.value),
+                        condition(searchTerm, cellValue, row, column) {
+                            return searchTerm.includes(cellValue);
+                        },
+                        selectOptions: cd.multiselectFilterOptions,
+                        $$selectOptionsMapping: cd.multiselectFilterOptions.reduce((a, v) => Object.assign(a, {[v.value]: v.label}), {}),
+                        $$multiselectFilterTooltip() {
+                            const prefix = 'Active filter';
+                            switch (this.term.length) {
+                                case 0:
+                                    return `${prefix}: show none`;
+                                default:
+                                    return `${prefix}: ${this.term.map((t) => this.$$selectOptionsMapping[t]).join(', ')}`;
+                                case this.selectOptions.length:
+                                    return `${prefix}: show all`;
+                            }
+                        }
+                    };
+                    if (!cd.cellTemplate) {
+                        cd.cellTemplate = `
+                            <div class="ui-grid-cell-contents">
+                                {{ col.colDef.filter.$$selectOptionsMapping[row.entity[col.field]] }}
+                            </div>
+                        `;
+                    }
+                });
+            }
+        }
+    } as ng.IDirective;
+}
+
+pcUiGridFilters.$inject = ['uiGridConstants'];
diff --git a/modules/frontend/app/configuration/components/pc-ui-grid-filters/index.ts b/modules/frontend/app/configuration/components/pc-ui-grid-filters/index.ts
new file mode 100644
index 0000000..a34a78a
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pc-ui-grid-filters/index.ts
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import directive from './directive';
+import flow from 'lodash/flow';
+
+export default angular
+    .module('ignite-console.page-configure.pc-ui-grid-filters', ['ui.grid'])
+    .decorator('$tooltip', ['$delegate', ($delegate) => {
+        return function(el, config) {
+            const instance = $delegate(el, config);
+            instance.$referenceElement = el;
+            instance.destroy = flow(instance.destroy, () => instance.$referenceElement = null);
+            instance.$applyPlacement = flow(instance.$applyPlacement, () => {
+                if (!instance.$element)
+                    return;
+
+                const refWidth = instance.$referenceElement[0].getBoundingClientRect().width;
+                const elWidth = instance.$element[0].getBoundingClientRect().width;
+
+                if (refWidth > elWidth) {
+                    instance.$element.css({
+                        width: refWidth,
+                        maxWidth: 'initial'
+                    });
+                }
+            });
+            return instance;
+        };
+    }])
+    .directive('pcUiGridFilters', directive);
diff --git a/modules/frontend/app/configuration/components/pc-ui-grid-filters/style.scss b/modules/frontend/app/configuration/components/pc-ui-grid-filters/style.scss
new file mode 100644
index 0000000..cbecc68
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pc-ui-grid-filters/style.scss
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.pc-ui-grid-filters {
+    // Decrease horizontal padding because multiselect button already has it
+    padding-left: 8px !important;
+    padding-right: 8px !important;
+}
\ No newline at end of file
diff --git a/modules/frontend/app/configuration/components/pc-ui-grid-filters/template.pug b/modules/frontend/app/configuration/components/pc-ui-grid-filters/template.pug
new file mode 100644
index 0000000..e39742d
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pc-ui-grid-filters/template.pug
@@ -0,0 +1,39 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.ui-grid-filter-container.pc-ui-grid-filters(
+    role='columnheader'
+    ng-style='col.extraStyle'
+    ng-repeat='colFilter in col.filters'
+    ng-class="{'ui-grid-filter-cancel-button-hidden' : colFilter.disableCancelFilterButton === true }"
+    ng-switch='colFilter.type'
+)
+    div(ng-switch-when='select')
+        button.btn-ignite.btn-ignite--link-dashed-success(
+            ng-class=`{
+                'btn-ignite--link-dashed-success': colFilter.term.length === colFilter.selectOptions.length,
+                'btn-ignite--link-dashed-primary': colFilter.term.length !== colFilter.selectOptions.length
+            }`
+            type='button'
+            title='{{ colFilter.$$multiselectFilterTooltip() }}'
+            ng-model='colFilter.term'
+            bs-select
+            bs-options='option.value as option.label for option in colFilter.selectOptions'
+            data-multiple='true'
+            data-trigger='click'
+            data-placement='bottom-right'
+            protect-from-bs-select-render
+        ) {{ col.displayName }}
diff --git a/modules/frontend/app/configuration/components/pcIsInCollection.ts b/modules/frontend/app/configuration/components/pcIsInCollection.ts
new file mode 100644
index 0000000..f348f3d
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pcIsInCollection.ts
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class Controller<T> {
+    ngModel: ng.INgModelController;
+    items: T[];
+
+    $onInit() {
+        this.ngModel.$validators.isInCollection = (item) => {
+            if (!item || !this.items)
+                return true;
+
+            return this.items.includes(item);
+        };
+    }
+
+    $onChanges() {
+        this.ngModel.$validate();
+    }
+}
+
+export default function pcIsInCollection() {
+    return {
+        controller: Controller,
+        require: {
+            ngModel: 'ngModel'
+        },
+        bindToController: {
+            items: '<pcIsInCollection'
+        }
+    };
+}
diff --git a/modules/frontend/app/configuration/components/pcValidation.ts b/modules/frontend/app/configuration/components/pcValidation.ts
new file mode 100644
index 0000000..b67dd30
--- /dev/null
+++ b/modules/frontend/app/configuration/components/pcValidation.ts
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import LegacyUtilsFactory from 'app/services/LegacyUtils.service';
+
+export default angular.module('ignite-console.page-configure.validation', [])
+    .directive('pcNotInCollection', function() {
+        class Controller<T> {
+            ngModel: ng.INgModelController;
+            items: T[];
+
+            $onInit() {
+                this.ngModel.$validators.notInCollection = (item: T) => {
+                    if (!this.items)
+                        return true;
+
+                    return !this.items.includes(item);
+                };
+            }
+
+            $onChanges() {
+                this.ngModel.$validate();
+            }
+        }
+
+        return {
+            controller: Controller,
+            require: {
+                ngModel: 'ngModel'
+            },
+            bindToController: {
+                items: '<pcNotInCollection'
+            }
+        };
+    })
+    .directive('pcInCollection', function() {
+        class Controller<T> {
+            ngModel: ng.INgModelController;
+            items: T[];
+            pluck?: string;
+
+            $onInit() {
+                this.ngModel.$validators.inCollection = (item: T) => {
+                    if (!this.items)
+                        return false;
+
+                    const items = this.pluck ? this.items.map((i) => i[this.pluck]) : this.items;
+                    return Array.isArray(item)
+                        ? item.every((i) => items.includes(i))
+                        : items.includes(item);
+                };
+            }
+
+            $onChanges() {
+                this.ngModel.$validate();
+            }
+        }
+
+        return {
+            controller: Controller,
+            require: {
+                ngModel: 'ngModel'
+            },
+            bindToController: {
+                items: '<pcInCollection',
+                pluck: '@?pcInCollectionPluck'
+            }
+        };
+    })
+    .directive('pcPowerOfTwo', function() {
+        class Controller {
+            ngModel: ng.INgModelController;
+            $onInit() {
+                this.ngModel.$validators.powerOfTwo = (value: number) => {
+                    return !value || ((value & -value) === value);
+                };
+            }
+        }
+
+        return {
+            controller: Controller,
+            require: {
+                ngModel: 'ngModel'
+            },
+            bindToController: true
+        };
+    })
+    .directive('isValidJavaIdentifier', ['IgniteLegacyUtils', function(LegacyUtils: ReturnType<typeof LegacyUtilsFactory>) {
+        return {
+            link(scope, el, attr, ngModel: ng.INgModelController) {
+                ngModel.$validators.isValidJavaIdentifier = (value: string) => LegacyUtils.VALID_JAVA_IDENTIFIER.test(value);
+            },
+            require: 'ngModel'
+        };
+    }])
+    .directive('notJavaReservedWord', ['IgniteLegacyUtils', function(LegacyUtils: ReturnType<typeof LegacyUtilsFactory>) {
+        return {
+            link(scope, el, attr, ngModel: ng.INgModelController) {
+                ngModel.$validators.notJavaReservedWord = (value: string) => !LegacyUtils.JAVA_KEYWORDS.includes(value);
+            },
+            require: 'ngModel'
+        };
+    }]);
diff --git a/modules/frontend/app/configuration/components/preview-panel/directive.ts b/modules/frontend/app/configuration/components/preview-panel/directive.ts
new file mode 100644
index 0000000..0845df9
--- /dev/null
+++ b/modules/frontend/app/configuration/components/preview-panel/directive.ts
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import ace from 'brace';
+import _ from 'lodash';
+
+previewPanelDirective.$inject = ['$interval', '$timeout'];
+
+export default function previewPanelDirective($interval: ng.IIntervalService, $timeout: ng.ITimeoutService) {
+    let animation = {editor: null, stage: 0, start: 0, stop: 0};
+    let prevContent = [];
+
+    const Range = ace.acequire('ace/range').Range;
+
+    const _clearSelection = (editor) => {
+        _.forEach(editor.session.getMarkers(false), (marker) => {
+            editor.session.removeMarker(marker.id);
+        });
+    };
+
+    /**
+     * Switch to next stage of animation.
+     */
+    const _animate = () => {
+        animation.stage += animation.step;
+
+        const stage = animation.stage;
+        const editor = animation.editor;
+
+        _clearSelection(editor);
+
+        animation.selections.forEach((selection) => {
+            editor.session.addMarker(new Range(selection.start, 0, selection.stop, 0),
+                'preview-highlight-' + stage, 'line', false);
+        });
+
+        if (stage === animation.finalStage) {
+            editor.animatePromise = null;
+
+            if (animation.clearOnFinal)
+                _clearSelection(editor);
+        }
+    };
+
+    /**
+     * Selection with animation.
+     *
+     * @param editor Editor to show selection animation.
+     * @param selections Array of selection intervals.
+     * @param step Step of animation (1 or -1).
+     * @param stage Start stage of animation.
+     * @param finalStage Final stage of animation.
+     * @param clearOnFinal Boolean flat to clear selection on animation finish.
+     */
+    const _fade = (editor, selections, step, stage, finalStage, clearOnFinal) => {
+        const promise = editor.animatePromise;
+
+        if (promise) {
+            $interval.cancel(promise);
+
+            _clearSelection(editor);
+        }
+
+        animation = {editor, selections, step, stage, finalStage, clearOnFinal};
+
+        editor.animatePromise = $interval(_animate, 100, 10, false);
+    };
+
+    /**
+     * Show selections with animation.
+     *
+     * @param editor Editor to show selection.
+     * @param selections Array of selection intervals.
+     */
+    const _fadeIn = (editor, selections) => {
+        _fade(editor, selections, 1, 0, 10, false);
+    };
+
+    /**
+     * Hide selections with animation.
+     *
+     * @param editor Editor to show selection.
+     * @param selections Array of selection intervals.
+     */
+    const _fadeOut = (editor, selections) => {
+        _fade(editor, selections, -1, 10, 0, true);
+    };
+
+    const onChange = ([content, editor]) => {
+        const {clearPromise} = editor;
+        const {lines} = content;
+
+        if (content.action === 'remove')
+            prevContent = lines;
+        else if (prevContent.length > 0 && lines.length > 0 && editor.attractAttention) {
+            if (clearPromise) {
+                $timeout.cancel(clearPromise);
+
+                _clearSelection(editor);
+            }
+
+            const selections = [];
+
+            let newIx = 0;
+            let prevIx = 0;
+
+            let prevLen = prevContent.length - (prevContent[prevContent.length - 1] === '' ? 1 : 0);
+            let newLen = lines.length - (lines[lines.length - 1] === '' ? 1 : 0);
+
+            const removed = newLen < prevLen;
+
+            let skipEnd = 0;
+
+            let selected = false;
+            let scrollTo = -1;
+
+            while (lines[newLen - 1] === prevContent[prevLen - 1] && newLen > 0 && prevLen > 0) {
+                prevLen -= 1;
+                newLen -= 1;
+
+                skipEnd += 1;
+            }
+
+            while (newIx < newLen || prevIx < prevLen) {
+                let start = -1;
+                let stop = -1;
+
+                // Find an index of a first line with different text.
+                for (; (newIx < newLen || prevIx < prevLen) && start < 0; newIx++, prevIx++) {
+                    if (newIx >= newLen || prevIx >= prevLen || lines[newIx] !== prevContent[prevIx]) {
+                        start = newIx;
+
+                        break;
+                    }
+                }
+
+                if (start >= 0) {
+                    // Find an index of a last line with different text by checking last string of old and new content in reverse order.
+                    for (let i = start; i < newLen && stop < 0; i++) {
+                        for (let j = prevIx; j < prevLen && stop < 0; j++) {
+                            if (lines[i] === prevContent[j] && lines[i] !== '') {
+                                stop = i;
+
+                                newIx = i;
+                                prevIx = j;
+
+                                break;
+                            }
+                        }
+                    }
+
+                    if (stop < 0) {
+                        stop = newLen;
+
+                        newIx = newLen;
+                        prevIx = prevLen;
+                    }
+
+                    if (start === stop) {
+                        if (removed)
+                            start = Math.max(0, start - 1);
+
+                        stop = Math.min(newLen + skipEnd, stop + 1);
+                    }
+
+                    if (start <= stop) {
+                        selections.push({start, stop});
+
+                        if (!selected)
+                            scrollTo = start;
+
+                        selected = true;
+                    }
+                }
+            }
+
+            // Run clear selection one time.
+            if (selected) {
+                _fadeIn(editor, selections);
+
+                editor.clearPromise = $timeout(() => {
+                    _fadeOut(editor, selections);
+
+                    editor.clearPromise = null;
+                }, 2000);
+
+                editor.scrollToRow(scrollTo);
+            }
+
+            prevContent = [];
+        }
+        else
+            editor.attractAttention = true;
+    };
+
+
+    const link = (scope, $element, $attrs, [igniteUiAceTabs1, igniteUiAceTabs2]) => {
+        const igniteUiAceTabs = igniteUiAceTabs1 || igniteUiAceTabs2;
+
+        if (!igniteUiAceTabs)
+            return;
+
+        igniteUiAceTabs.onLoad = (editor) => {
+            editor.setReadOnly(true);
+            editor.setOption('highlightActiveLine', false);
+            editor.setAutoScrollEditorIntoView(true);
+            editor.$blockScrolling = Infinity;
+            editor.attractAttention = false;
+
+            const renderer = editor.renderer;
+
+            renderer.setHighlightGutterLine(false);
+            renderer.setShowPrintMargin(false);
+            renderer.setOption('fontSize', '10px');
+            renderer.setOption('maxLines', '50');
+
+            editor.setTheme('ace/theme/chrome');
+        };
+
+        igniteUiAceTabs.onChange = onChange;
+    };
+
+    return {
+        restrict: 'C',
+        link,
+        require: ['?igniteUiAceTabs', '?^igniteUiAceTabs']
+    };
+}
diff --git a/modules/frontend/app/configuration/components/preview-panel/index.ts b/modules/frontend/app/configuration/components/preview-panel/index.ts
new file mode 100644
index 0000000..ff1367b
--- /dev/null
+++ b/modules/frontend/app/configuration/components/preview-panel/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import directive from './directive';
+
+export default angular
+    .module('ignite-console.page-configure.preview-panel', [])
+    .directive('previewPanel', directive);
diff --git a/modules/frontend/app/configuration/components/ui-ace-java/index.ts b/modules/frontend/app/configuration/components/ui-ace-java/index.ts
new file mode 100644
index 0000000..3d0b9b6
--- /dev/null
+++ b/modules/frontend/app/configuration/components/ui-ace-java/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import UiAceJavaDirective from './ui-ace-java.directive';
+
+export default angular.module('ignite-console.ui-ace-java', [
+    'ignite-console.services',
+    'ignite-console.configuration.generator'
+])
+.directive('igniteUiAceJava', UiAceJavaDirective);
diff --git a/modules/frontend/app/configuration/components/ui-ace-java/ui-ace-java.controller.ts b/modules/frontend/app/configuration/components/ui-ace-java/ui-ace-java.controller.ts
new file mode 100644
index 0000000..2d636c0
--- /dev/null
+++ b/modules/frontend/app/configuration/components/ui-ace-java/ui-ace-java.controller.ts
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import IgniteUiAceGeneratorFactory from '../ui-ace.controller';
+
+export default class IgniteUiAceJava extends IgniteUiAceGeneratorFactory {
+    static $inject = ['$scope', '$attrs', 'IgniteVersion', 'JavaTransformer'];
+}
diff --git a/modules/frontend/app/configuration/components/ui-ace-java/ui-ace-java.directive.ts b/modules/frontend/app/configuration/components/ui-ace-java/ui-ace-java.directive.ts
new file mode 100644
index 0000000..b4f945d
--- /dev/null
+++ b/modules/frontend/app/configuration/components/ui-ace-java/ui-ace-java.directive.ts
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './ui-ace-java.pug';
+import IgniteUiAceJava from './ui-ace-java.controller';
+
+export default function() {
+    return {
+        priority: 1,
+        restrict: 'E',
+        scope: {
+            master: '=',
+            detail: '='
+        },
+        bindToController: {
+            data: '=?ngModel',
+            generator: '@',
+            client: '@'
+        },
+        template,
+        controller: IgniteUiAceJava,
+        controllerAs: 'ctrl',
+        require: {
+            ctrl: 'igniteUiAceJava',
+            igniteUiAceTabs: '?^igniteUiAceTabs',
+            formCtrl: '?^form',
+            ngModelCtrl: '?ngModel'
+        }
+    };
+}
diff --git a/modules/frontend/app/configuration/components/ui-ace-java/ui-ace-java.pug b/modules/frontend/app/configuration/components/ui-ace-java/ui-ace-java.pug
new file mode 100644
index 0000000..5acffb8
--- /dev/null
+++ b/modules/frontend/app/configuration/components/ui-ace-java/ui-ace-java.pug
@@ -0,0 +1,22 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+div(ng-if='ctrl.data' 
+    ignite-ace='{onLoad: onLoad, \
+             onChange: onChange, \
+             renderOptions: renderOptions, \
+             mode: "java"}' 
+    ng-model='ctrl.data')
diff --git a/modules/frontend/app/configuration/components/ui-ace-spring/index.ts b/modules/frontend/app/configuration/components/ui-ace-spring/index.ts
new file mode 100644
index 0000000..938dd6f
--- /dev/null
+++ b/modules/frontend/app/configuration/components/ui-ace-spring/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import UiAceSpringDirective from './ui-ace-spring.directive';
+
+export default angular.module('ignite-console.ui-ace-spring', [
+    'ignite-console.services',
+    'ignite-console.configuration.generator'
+])
+.directive('igniteUiAceSpring', UiAceSpringDirective);
diff --git a/modules/frontend/app/configuration/components/ui-ace-spring/ui-ace-spring.controller.ts b/modules/frontend/app/configuration/components/ui-ace-spring/ui-ace-spring.controller.ts
new file mode 100644
index 0000000..09c351c
--- /dev/null
+++ b/modules/frontend/app/configuration/components/ui-ace-spring/ui-ace-spring.controller.ts
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import IgniteUiAceGeneratorFactory from '../ui-ace.controller';
+
+export default class IgniteUiAceSpring extends IgniteUiAceGeneratorFactory {
+    static $inject = ['$scope', '$attrs', 'IgniteVersion', 'SpringTransformer'];
+}
diff --git a/modules/frontend/app/configuration/components/ui-ace-spring/ui-ace-spring.directive.ts b/modules/frontend/app/configuration/components/ui-ace-spring/ui-ace-spring.directive.ts
new file mode 100644
index 0000000..532ba42
--- /dev/null
+++ b/modules/frontend/app/configuration/components/ui-ace-spring/ui-ace-spring.directive.ts
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './ui-ace-spring.pug';
+import IgniteUiAceSpring from './ui-ace-spring.controller';
+
+export default function() {
+    return {
+        priority: 1,
+        restrict: 'E',
+        scope: {
+            master: '=',
+            detail: '='
+        },
+        bindToController: {
+            data: '=?ngModel',
+            generator: '@',
+            client: '@'
+        },
+        template,
+        controller: IgniteUiAceSpring,
+        controllerAs: 'ctrl',
+        require: {
+            ctrl: 'igniteUiAceSpring',
+            igniteUiAceTabs: '?^igniteUiAceTabs',
+            formCtrl: '?^form',
+            ngModelCtrl: '?ngModel'
+        }
+    };
+}
diff --git a/modules/frontend/app/configuration/components/ui-ace-spring/ui-ace-spring.pug b/modules/frontend/app/configuration/components/ui-ace-spring/ui-ace-spring.pug
new file mode 100644
index 0000000..0dd627a
--- /dev/null
+++ b/modules/frontend/app/configuration/components/ui-ace-spring/ui-ace-spring.pug
@@ -0,0 +1,17 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+div(ng-if='ctrl.data' ignite-ace='{onLoad: onLoad, onChange: onChange, mode: "xml"}' ng-model='ctrl.data')
diff --git a/modules/frontend/app/configuration/components/ui-ace-tabs.directive.ts b/modules/frontend/app/configuration/components/ui-ace-tabs.directive.ts
new file mode 100644
index 0000000..a174b98
--- /dev/null
+++ b/modules/frontend/app/configuration/components/ui-ace-tabs.directive.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+export default function() {
+    return {
+        scope: true,
+        restrict: 'AE',
+        controller: _.noop
+    };
+}
diff --git a/modules/frontend/app/configuration/components/ui-ace.controller.js b/modules/frontend/app/configuration/components/ui-ace.controller.js
new file mode 100644
index 0000000..3b35b2a
--- /dev/null
+++ b/modules/frontend/app/configuration/components/ui-ace.controller.js
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class IgniteUiAceGeneratorFactory {
+    constructor($scope, $attrs, Version, generatorFactory) {
+        this.scope = $scope;
+        this.attrs = $attrs;
+        this.Version = Version;
+        this.generatorFactory = generatorFactory;
+    }
+
+    $onInit() {
+        delete this.data;
+
+        const available = this.Version.available.bind(this.Version);
+
+        // Setup generator.
+        switch (this.generator) {
+            case 'igniteConfiguration':
+                this.generate = (cluster) => this.generatorFactory.cluster(cluster, this.Version.currentSbj.getValue(), this.client === 'true');
+
+                break;
+            case 'cacheStore':
+            case 'cacheQuery':
+                this.generate = (cache, domains) => {
+                    const cacheDomains = _.reduce(domains, (acc, domain) => {
+                        if (_.includes(cache.domains, domain.value))
+                            acc.push(domain.meta);
+
+                        return acc;
+                    }, []);
+
+                    return this.generatorFactory[this.generator](cache, cacheDomains, available);
+                };
+
+                break;
+            case 'cacheNodeFilter':
+                this.generate = (cache, igfss) => {
+                    const cacheIgfss = _.reduce(igfss, (acc, igfs) => {
+                        acc.push(igfs.igfs);
+
+                        return acc;
+                    }, []);
+
+                    return this.generatorFactory.cacheNodeFilter(cache, cacheIgfss);
+                };
+
+                break;
+            case 'clusterServiceConfiguration':
+                this.generate = (cluster, caches) => {
+                    return this.generatorFactory.clusterServiceConfiguration(cluster.serviceConfigurations, caches);
+                };
+
+                break;
+            case 'clusterCheckpoint':
+                this.generate = (cluster, caches) => {
+                    return this.generatorFactory.clusterCheckpoint(cluster, available, caches);
+                };
+
+                break;
+            case 'igfss':
+                this.generate = (cluster, igfss) => {
+                    const clusterIgfss = _.reduce(igfss, (acc, igfs) => {
+                        if (_.includes(cluster.igfss, igfs.value))
+                            acc.push(igfs.igfs);
+
+                        return acc;
+                    }, []);
+
+                    return this.generatorFactory.clusterIgfss(clusterIgfss, available);
+                };
+
+                break;
+            default:
+                this.generate = (master) => this.generatorFactory[this.generator](master, available);
+        }
+    }
+
+    $postLink() {
+        if (this.formCtrl && this.ngModelCtrl)
+            this.formCtrl.$removeControl(this.ngModelCtrl);
+
+        if (this.igniteUiAceTabs && this.igniteUiAceTabs.onLoad) {
+            this.scope.onLoad = (editor) => {
+                this.igniteUiAceTabs.onLoad(editor);
+
+                this.scope.$watch('master', () => editor.attractAttention = false);
+            };
+        }
+
+        if (this.igniteUiAceTabs && this.igniteUiAceTabs.onChange)
+            this.scope.onChange = this.igniteUiAceTabs.onChange;
+
+        const noDeepWatch = !(typeof this.attrs.noDeepWatch !== 'undefined');
+
+        const next = () => {
+            this.ctrl.data = _.isNil(this.scope.master) ? null : this.ctrl.generate(this.scope.master, this.scope.detail).asString();
+        };
+
+        // Setup watchers.
+        this.scope.$watch('master', next, noDeepWatch);
+
+        this.subscription = this.Version.currentSbj.subscribe({next});
+    }
+
+    $onDestroy() {
+        this.subscription.unsubscribe();
+    }
+}
diff --git a/modules/frontend/app/configuration/defaultNames.ts b/modules/frontend/app/configuration/defaultNames.ts
new file mode 100644
index 0000000..abc3afc
--- /dev/null
+++ b/modules/frontend/app/configuration/defaultNames.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const defaultNames = {
+    cluster: 'Cluster',
+    cache: 'Cache',
+    igfs: 'IGFS',
+    importedCluster: 'ImportedCluster'
+};
diff --git a/modules/frontend/app/configuration/generator/JavaTypesNonEnum.service.spec.ts b/modules/frontend/app/configuration/generator/JavaTypesNonEnum.service.spec.ts
new file mode 100644
index 0000000..c63977f
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/JavaTypesNonEnum.service.spec.ts
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {JavaTypesNonEnum} from './JavaTypesNonEnum.service';
+
+import ClusterDflts from './generator/defaults/Cluster.service';
+import CacheDflts from './generator/defaults/Cache.service';
+import IgfsDflts from './generator/defaults/IGFS.service';
+import JavaTypes from 'app/services/JavaTypes.service';
+import {assert} from 'chai';
+
+const instance = new JavaTypesNonEnum(new ClusterDflts(), new CacheDflts(), new IgfsDflts(), new JavaTypes());
+
+suite('JavaTypesNonEnum', () => {
+    test('nonEnum', () => {
+        assert.equal(instance.nonEnum('org.apache.ignite.cache.CacheMode'), false);
+        assert.equal(instance.nonEnum('org.apache.ignite.transactions.TransactionConcurrency'), false);
+        assert.equal(instance.nonEnum('org.apache.ignite.cache.CacheWriteSynchronizationMode'), false);
+        assert.equal(instance.nonEnum('org.apache.ignite.igfs.IgfsIpcEndpointType'), false);
+        assert.equal(instance.nonEnum('java.io.Serializable'), true);
+        assert.equal(instance.nonEnum('BigDecimal'), true);
+    });
+});
diff --git a/modules/frontend/app/configuration/generator/JavaTypesNonEnum.service.ts b/modules/frontend/app/configuration/generator/JavaTypesNonEnum.service.ts
new file mode 100644
index 0000000..aa08ae2
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/JavaTypesNonEnum.service.ts
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import uniq from 'lodash/uniq';
+import map from 'lodash/map';
+import reduce from 'lodash/reduce';
+import isObject from 'lodash/isObject';
+import merge from 'lodash/merge';
+import includes from 'lodash/includes';
+
+export class JavaTypesNonEnum {
+    static $inject = ['IgniteClusterDefaults', 'IgniteCacheDefaults', 'IgniteIGFSDefaults', 'JavaTypes'];
+
+    enumClasses: any;
+    shortEnumClasses: any;
+
+    constructor(clusterDflts, cacheDflts, igfsDflts, JavaTypes) {
+        this.enumClasses = uniq(this._enumClassesAcc(merge(clusterDflts, cacheDflts, igfsDflts), []));
+        this.shortEnumClasses = map(this.enumClasses, (cls) => JavaTypes.shortClassName(cls));
+    }
+
+    /**
+     * Check if class name is non enum class in Ignite configuration.
+     *
+     * @param clsName
+     * @return {boolean}
+     */
+    nonEnum(clsName: string): boolean {
+        return !includes(this.shortEnumClasses, clsName) && !includes(this.enumClasses, clsName);
+    }
+
+    /**
+     * Collects recursive enum classes.
+     *
+     * @param root Root object.
+     * @param classes Collected classes.
+     */
+    private _enumClassesAcc(root, classes): Array<string> {
+        return reduce(root, (acc, val, key) => {
+            if (key === 'clsName')
+                acc.push(val);
+            else if (isObject(val))
+                this._enumClassesAcc(val, acc);
+
+            return acc;
+        }, classes);
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/configuration.module.js b/modules/frontend/app/configuration/generator/configuration.module.js
new file mode 100644
index 0000000..3bd9024
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/configuration.module.js
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {JavaTypesNonEnum} from './JavaTypesNonEnum.service';
+import IgniteClusterDefaults from './generator/defaults/Cluster.service';
+import IgniteClusterPlatformDefaults from './generator/defaults/Cluster.platform.service';
+import IgniteCacheDefaults from './generator/defaults/Cache.service';
+import IgniteCachePlatformDefaults from './generator/defaults/Cache.platform.service';
+import IgniteIGFSDefaults from './generator/defaults/IGFS.service';
+import IgniteEventGroups from './generator/defaults/Event-groups.service';
+
+import IgniteConfigurationGenerator from './generator/ConfigurationGenerator';
+import IgnitePlatformGenerator from './generator/PlatformGenerator';
+
+import IgniteSpringTransformer from './generator/SpringTransformer.service';
+import IgniteJavaTransformer from './generator/JavaTransformer.service';
+import SharpTransformer from './generator/SharpTransformer.service';
+import IgniteDockerGenerator from './generator/Docker.service';
+import IgniteMavenGenerator from './generator/Maven.service';
+import IgniteGeneratorProperties from './generator/Properties.service';
+import IgniteReadmeGenerator from './generator/Readme.service';
+import IgniteCustomGenerator from './generator/Custom.service';
+import IgniteArtifactVersionUtils from './generator/ArtifactVersionChecker.service';
+
+// Ignite events groups.
+export default angular
+    .module('ignite-console.configuration.generator', [])
+    .service('JavaTypesNonEnum', JavaTypesNonEnum)
+    .service('IgniteConfigurationGenerator', function() { return IgniteConfigurationGenerator;})
+    .service('IgnitePlatformGenerator', IgnitePlatformGenerator)
+    .service('SpringTransformer', function() { return IgniteSpringTransformer;})
+    .service('JavaTransformer', function() { return IgniteJavaTransformer;})
+    .service('IgniteSharpTransformer', SharpTransformer)
+    .service('IgniteEventGroups', IgniteEventGroups)
+    .service('IgniteClusterDefaults', IgniteClusterDefaults)
+    .service('IgniteClusterPlatformDefaults', IgniteClusterPlatformDefaults)
+    .service('IgniteCacheDefaults', IgniteCacheDefaults)
+    .service('IgniteCachePlatformDefaults', IgniteCachePlatformDefaults)
+    .service('IgniteIGFSDefaults', IgniteIGFSDefaults)
+    .service('IgnitePropertiesGenerator', IgniteGeneratorProperties)
+    .service('IgniteReadmeGenerator', IgniteReadmeGenerator)
+    .service('IgniteDockerGenerator', IgniteDockerGenerator)
+    .service('IgniteMavenGenerator', IgniteMavenGenerator)
+    .service('IgniteCustomGenerator', IgniteCustomGenerator)
+    .service('IgniteArtifactVersionUtils', IgniteArtifactVersionUtils);
diff --git a/modules/frontend/app/configuration/generator/generator/AbstractTransformer.js b/modules/frontend/app/configuration/generator/generator/AbstractTransformer.js
new file mode 100644
index 0000000..e765379
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/AbstractTransformer.js
@@ -0,0 +1,423 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import StringBuilder from './StringBuilder';
+
+import IgniteConfigurationGenerator from './ConfigurationGenerator';
+
+import IgniteClusterDefaults from './defaults/Cluster.service';
+import IgniteCacheDefaults from './defaults/Cache.service';
+import IgniteIGFSDefaults from './defaults/IGFS.service';
+
+import JavaTypes from '../../../services/JavaTypes.service';
+import {JavaTypesNonEnum} from '../JavaTypesNonEnum.service';
+
+const clusterDflts = new IgniteClusterDefaults();
+const cacheDflts = new IgniteCacheDefaults();
+const igfsDflts = new IgniteIGFSDefaults();
+
+export default class AbstractTransformer {
+    static generator = IgniteConfigurationGenerator;
+    static javaTypes = new JavaTypes();
+    static javaTypesNonEnum = new JavaTypesNonEnum(clusterDflts, cacheDflts, igfsDflts, new JavaTypes());
+
+    // Append comment with time stamp.
+    static mainComment(sb, ...lines) {
+        lines.push(sb.generatedBy());
+
+        return this.commentBlock(sb, ...lines);
+    }
+
+    // Append line before and after property.
+    static _emptyLineIfNeeded(sb, props, curIdx) {
+        if (curIdx === props.length - 1)
+            return;
+
+        const cur = props[curIdx];
+
+        // Empty line after.
+        if (_.includes(['MAP', 'COLLECTION', 'ARRAY'], cur.clsName) || (cur.clsName === 'BEAN' && cur.value.isComplex()))
+            return sb.emptyLine();
+
+        const next = props[curIdx + 1];
+
+        // Empty line before.
+        if (_.includes(['MAP', 'COLLECTION', 'ARRAY'], next.clsName) || (next.clsName === 'BEAN' && next.value.isComplex()))
+            return sb.emptyLine();
+    }
+
+    // Generate general section.
+    static clusterGeneral(cluster, available) {
+        return this.toSection(this.generator.clusterGeneral(cluster, available));
+    }
+
+    // Generate atomics group.
+    static clusterAtomics(atomics, available) {
+        return this.toSection(this.generator.clusterAtomics(atomics, available));
+    }
+
+    // Generate binary group.
+    static clusterBinary(binary) {
+        return this.toSection(this.generator.clusterBinary(binary));
+    }
+
+    // Generate cache key configurations.
+    static clusterCacheKeyConfiguration(keyCfgs) {
+        return this.toSection(this.generator.clusterCacheKeyConfiguration(keyCfgs));
+    }
+
+    // Generate client connector configuration.
+    static clusterClientConnector(cluster, available) {
+        return this.toSection(this.generator.clusterClientConnector(cluster, available));
+    }
+
+    // Generate collision group.
+    static clusterCollision(collision) {
+        return this.toSection(this.generator.clusterCollision(collision));
+    }
+
+    // Generate communication group.
+    static clusterCommunication(cluster, available) {
+        return this.toSection(this.generator.clusterCommunication(cluster, available));
+    }
+
+    // Generate REST access configuration.
+    static clusterConnector(connector) {
+        return this.toSection(this.generator.clusterConnector(connector));
+    }
+
+    // Generate deployment group.
+    static clusterDeployment(cluster, available) {
+        return this.toSection(this.generator.clusterDeployment(cluster, available));
+    }
+
+    // Generate discovery group.
+    static clusterDiscovery(disco, available) {
+        return this.toSection(this.generator.clusterDiscovery(disco, available));
+    }
+
+    // Generate events group.
+    static clusterEvents(cluster, available) {
+        return this.toSection(this.generator.clusterEvents(cluster, available));
+    }
+
+    // Generate failover group.
+    static clusterFailover(cluster, available) {
+        return this.toSection(this.generator.clusterFailover(cluster, available));
+    }
+
+    // Generate hadoop group.
+    static clusterHadoop(hadoop) {
+        return this.toSection(this.generator.clusterHadoop(hadoop));
+    }
+
+    // Generate cluster IGFSs group.
+    static clusterIgfss(igfss, available) {
+        return this.toSection(this.generator.clusterIgfss(igfss, available));
+    }
+
+    // Generate load balancing SPI group.
+    static clusterLoadBalancing(cluster) {
+        return this.toSection(this.generator.clusterLoadBalancing(cluster));
+    }
+
+    // Generate logger group.
+    static clusterLogger(cluster) {
+        return this.toSection(this.generator.clusterLogger(cluster));
+    }
+
+    // Generate memory configuration group.
+    static clusterMemory(memoryConfiguration, available) {
+        return this.toSection(this.generator.clusterMemory(memoryConfiguration, available));
+    }
+
+    // Generate memory configuration group.
+    static clusterDataStorageConfiguration(cluster, available) {
+        return this.toSection(this.generator.clusterDataStorageConfiguration(cluster, available));
+    }
+
+    // Generate marshaller group.
+    static clusterMisc(cluster, available) {
+        return this.toSection(this.generator.clusterMisc(cluster, available));
+    }
+
+    // Generate marshaller group.
+    static clusterMvcc(cluster, available) {
+        return this.toSection(this.generator.clusterMvcc(cluster, available));
+    }
+
+    // Generate encryption group.
+    static clusterEncryption(encryption, available) {
+        return this.toSection(this.generator.clusterEncryption(encryption, available));
+    }
+
+    // Generate marshaller group.
+    static clusterMarshaller(cluster, available) {
+        return this.toSection(this.generator.clusterMarshaller(cluster, available));
+    }
+
+    // Generate metrics group.
+    static clusterMetrics(cluster, available) {
+        return this.toSection(this.generator.clusterMetrics(cluster, available));
+    }
+
+    // Generate ODBC group.
+    static clusterODBC(odbc, available) {
+        return this.toSection(this.generator.clusterODBC(odbc, available));
+    }
+
+    // Generate cluster persistence store group.
+    static clusterPersistence(persistence, available) {
+        return this.toSection(this.generator.clusterPersistence(persistence, available));
+    }
+
+    // Generate cluster query group.
+    static clusterQuery(cluster, available) {
+        return this.toSection(this.generator.clusterQuery(cluster, available));
+    }
+
+    // Generate cache node filter group.
+    static clusterServiceConfiguration(srvs, caches) {
+        return this.toSection(this.generator.clusterServiceConfiguration(srvs, caches));
+    }
+
+    // Generate ssl group.
+    static clusterSsl(cluster, available) {
+        return this.toSection(this.generator.clusterSsl(cluster, available));
+    }
+
+    // Generate swap group.
+    static clusterSwap(cluster) {
+        return this.toSection(this.generator.clusterSwap(cluster));
+    }
+
+    // Generate time group.
+    static clusterTime(cluster, available) {
+        return this.toSection(this.generator.clusterTime(cluster, available));
+    }
+
+    // Generate thread pools group.
+    static clusterPools(cluster, available) {
+        return this.toSection(this.generator.clusterPools(cluster, available));
+    }
+
+    // Generate transactions group.
+    static clusterTransactions(transactionConfiguration, available) {
+        return this.toSection(this.generator.clusterTransactions(transactionConfiguration, available));
+    }
+
+    // Generate user attributes group.
+    static clusterUserAttributes(cluster) {
+        return this.toSection(this.generator.clusterUserAttributes(cluster));
+    }
+
+    // Generate IGFS general group.
+    static igfsGeneral(igfs, available) {
+        return this.toSection(this.generator.igfsGeneral(igfs, available));
+    }
+
+    // Generate IGFS secondary file system group.
+    static igfsSecondFS(igfs) {
+        return this.toSection(this.generator.igfsSecondFS(igfs));
+    }
+
+    // Generate IGFS IPC group.
+    static igfsIPC(igfs) {
+        return this.toSection(this.generator.igfsIPC(igfs));
+    }
+
+    // Generate IGFS fragmentizer group.
+    static igfsFragmentizer(igfs) {
+        return this.toSection(this.generator.igfsFragmentizer(igfs));
+    }
+
+    // Generate IGFS Dual mode group.
+    static igfsDualMode(igfs) {
+        return this.toSection(this.generator.igfsDualMode(igfs));
+    }
+
+    // Generate IGFS miscellaneous group.
+    static igfsMisc(igfs, available) {
+        return this.toSection(this.generator.igfsMisc(igfs, available));
+    }
+
+    // Generate cache general group.
+    static cacheGeneral(cache, available) {
+        return this.toSection(this.generator.cacheGeneral(cache, available));
+    }
+
+    // Generate cache memory group.
+    static cacheAffinity(cache, available) {
+        return this.toSection(this.generator.cacheAffinity(cache, available));
+    }
+
+    // Generate cache key configuration.
+    static cacheKeyConfiguration(cache, available) {
+        return this.toSection(this.generator.cacheKeyConfiguration(cache, available));
+    }
+
+    // Generate cache memory group.
+    static cacheMemory(cache, available) {
+        return this.toSection(this.generator.cacheMemory(cache, available));
+    }
+
+    // Generate cache queries & Indexing group.
+    static cacheQuery(cache, domains, available) {
+        return this.toSection(this.generator.cacheQuery(cache, domains, available));
+    }
+
+    // Generate cache store group.
+    static cacheStore(cache, domains, available) {
+        return this.toSection(this.generator.cacheStore(cache, domains, available));
+    }
+
+    // Generate cache concurrency control group.
+    static cacheConcurrency(cache, available) {
+        return this.toSection(this.generator.cacheConcurrency(cache, available));
+    }
+
+    // Generate cache misc group.
+    static cacheMisc(cache, available) {
+        return this.toSection(this.generator.cacheMisc(cache, available));
+    }
+
+    // Generate cache node filter group.
+    static cacheNodeFilter(cache, igfss) {
+        return this.toSection(this.generator.cacheNodeFilter(cache, igfss));
+    }
+
+    // Generate cache rebalance group.
+    static cacheRebalance(cache) {
+        return this.toSection(this.generator.cacheRebalance(cache));
+    }
+
+    // Generate server near cache group.
+    static cacheNearServer(cache, available) {
+        return this.toSection(this.generator.cacheNearServer(cache, available));
+    }
+
+    // Generate client near cache group.
+    static cacheNearClient(cache, available) {
+        return this.toSection(this.generator.cacheNearClient(cache, available));
+    }
+
+    // Generate cache statistics group.
+    static cacheStatistics(cache) {
+        return this.toSection(this.generator.cacheStatistics(cache));
+    }
+
+    // Generate caches configs.
+    static clusterCaches(cluster, available, caches, igfss, client) {
+        return this.toSection(this.generator.clusterCaches(cluster, caches, igfss, available, client));
+    }
+
+    // Generate caches configs.
+    static clusterCheckpoint(cluster, available, caches) {
+        return this.toSection(this.generator.clusterCheckpoint(cluster, available, caches));
+    }
+
+    // Generate domain model for general group.
+    static domainModelGeneral(domain) {
+        return this.toSection(this.generator.domainModelGeneral(domain));
+    }
+
+    // Generate domain model for query group.
+    static domainModelQuery(domain, available) {
+        return this.toSection(this.generator.domainModelQuery(domain, available));
+    }
+
+    // Generate domain model for store group.
+    static domainStore(domain) {
+        return this.toSection(this.generator.domainStore(domain));
+    }
+
+    /**
+     * Check if configuration contains properties.
+     *
+     * @param {Bean} bean
+     * @returns {Boolean}
+     */
+    static hasProperties(bean) {
+        const searchProps = (prop) => {
+            switch (prop.clsName) {
+                case 'BEAN':
+                    if (this.hasProperties(prop.value))
+                        return true;
+
+                    break;
+                case 'ARRAY':
+                case 'COLLECTION':
+                    if (_.find(prop.items, (item) => this.hasProperties(item)))
+                        return true;
+
+                    break;
+                case 'DATA_SOURCE':
+                case 'PROPERTY':
+                case 'PROPERTY_CHAR':
+                    return true;
+                default:
+            }
+
+            return false;
+        };
+
+        return _.isObject(bean) && (!!_.find(bean.arguments, searchProps) || !!_.find(bean.properties, searchProps));
+    }
+
+    /**
+     * Collect datasource beans.
+     *
+     * @param {Bean} bean
+     */
+    static collectDataSources(bean) {
+        const dataSources = _.reduce(bean.properties, (acc, prop) => {
+            switch (prop.clsName.toUpperCase()) {
+                case 'ARRAY':
+                    if (this._isBean(prop.typeClsName))
+                        _.forEach(prop.items, (item) => acc.push(...this.collectDataSources(item)));
+
+                    break;
+                case 'BEAN':
+                    acc.push(...this.collectDataSources(prop.value));
+
+                    break;
+                case 'DATA_SOURCE':
+                    acc.push(prop.value);
+
+                    break;
+                default:
+            }
+
+            return acc;
+        }, []);
+
+        return _.uniqBy(dataSources, (ds) => ds.id);
+    }
+
+    /**
+     * Transform to section.
+     *
+     * @param cfg
+     * @param sb
+     * @return {StringBuilder}
+     */
+    static toSection(cfg, sb = new StringBuilder()) {
+        this._setProperties(sb, cfg);
+
+        return sb;
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/ArtifactVersionChecker.service.js b/modules/frontend/app/configuration/generator/generator/ArtifactVersionChecker.service.js
new file mode 100644
index 0000000..0aa8ff1
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/ArtifactVersionChecker.service.js
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+export default class ArtifactVersionChecker {
+    /**
+     * Compare two numbers.
+     *
+     * @param a {Number} First number to compare
+     * @param b {Number} Second number to compare.
+     * @return {Number} 1 when a is greater then b, -1 when b is greater then a, 0 when a and b is equal.
+     */
+    static _numberComparator(a, b) {
+        return a > b ? 1 : a < b ? -1 : 0;
+    }
+
+    /**
+     * Compare to version.
+     *
+     * @param {Object} a first compared version.
+     * @param {Object} b second compared version.
+     * @returns {Number} 1 if a > b, 0 if versions equals, -1 if a < b
+     */
+    static _compare(a, b) {
+        for (let i = 0; i < a.length && i < b.length; i++) {
+            const res = this._numberComparator(a[i], b[i]);
+
+            if (res !== 0)
+                return res;
+        }
+
+        return 0;
+    }
+
+    /**
+     * Tries to parse JDBC driver version.
+     *
+     * @param {String} ver - String representation of version.
+     * @returns {Number[]} - Array of version parts.
+     */
+    static _parse(ver) {
+        return _.map(ver.split(/[.-]/), (v) => {
+            return v.startsWith('jre') ? parseInt(v.substring(3), 10) : parseInt(v, 10);
+        });
+    }
+
+    /**
+     * Stay only latest versions of the same dependencies.
+     *
+     * @param deps Array of dependencies.
+     */
+    static latestVersions(deps) {
+        return _.map(_.values(_.groupBy(_.uniqWith(deps, _.isEqual), (dep) => dep.groupId + dep.artifactId)), (arr) => {
+            if (arr.length > 1) {
+                try {
+                    return _.reduce(arr, (resDep, dep) => {
+                        if (this._compare(this._parse(dep.version), this._parse(resDep.version)) > 0)
+                            return dep;
+
+                        return resDep;
+                    });
+                }
+                catch (err) {
+                    return _.last(_.sortBy(arr, 'version'));
+                }
+            }
+
+            return arr[0];
+        });
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/Beans.js b/modules/frontend/app/configuration/generator/generator/Beans.js
new file mode 100644
index 0000000..02f6015
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/Beans.js
@@ -0,0 +1,424 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+import negate from 'lodash/negate';
+import isNil from 'lodash/isNil';
+import isEmpty from 'lodash/isEmpty';
+import mixin from 'lodash/mixin';
+
+const nonNil = negate(isNil);
+const nonEmpty = negate(isEmpty);
+
+mixin({
+    nonNil,
+    nonEmpty
+});
+
+export class EmptyBean {
+    /**
+     * @param {String} clsName
+     */
+    constructor(clsName) {
+        this.properties = [];
+        this.arguments = [];
+
+        this.clsName = clsName;
+    }
+
+    isEmpty() {
+        return false;
+    }
+
+    nonEmpty() {
+        return !this.isEmpty();
+    }
+
+    isComplex() {
+        return nonEmpty(this.properties) || !!_.find(this.arguments, (arg) => arg.clsName === 'MAP');
+    }
+
+    nonComplex() {
+        return !this.isComplex();
+    }
+
+    findProperty(name) {
+        return _.find(this.properties, {name});
+    }
+}
+
+export class Bean extends EmptyBean {
+    /**
+     * @param {String} clsName
+     * @param {String} id
+     * @param {Object} src
+     * @param {Object} dflts
+     */
+    constructor(clsName, id, src, dflts = {}) {
+        super(clsName);
+
+        this.id = id;
+
+        this.src = src;
+        this.dflts = dflts;
+    }
+
+    factoryMethod(name) {
+        this.factoryMtd = name;
+
+        return this;
+    }
+
+    /**
+     * @param acc
+     * @param clsName
+     * @param model
+     * @param name
+     * @param {Function} nonEmpty Non empty function.
+     * @param {Function} mapper Mapper function.
+     * @returns {Bean}
+     * @private
+     */
+    _property(acc, clsName, model, name, nonEmpty = () => true, mapper = (val) => val) {
+        if (!this.src)
+            return this;
+
+        const value = mapper(_.get(this.src, model));
+
+        if (nonEmpty(value) && value !== _.get(this.dflts, model))
+            acc.push({clsName, name, value});
+
+        return this;
+    }
+
+    isEmpty() {
+        return isEmpty(this.arguments) && isEmpty(this.properties);
+    }
+
+    constructorArgument(clsName, value) {
+        this.arguments.push({clsName, value});
+
+        return this;
+    }
+
+    stringConstructorArgument(model) {
+        return this._property(this.arguments, 'java.lang.String', model, null, nonEmpty);
+    }
+
+    intConstructorArgument(model) {
+        return this._property(this.arguments, 'int', model, null, nonNil);
+    }
+
+    boolConstructorArgument(model) {
+        return this._property(this.arguments, 'boolean', model, null, nonNil);
+    }
+
+    longConstructorArgument(model) {
+        return this._property(this.arguments, 'long', model, null, nonNil);
+    }
+
+    classConstructorArgument(model) {
+        return this._property(this.arguments, 'java.lang.Class', model, null, nonEmpty);
+    }
+
+    pathConstructorArgument(model) {
+        return this._property(this.arguments, 'PATH', model, null, nonEmpty);
+    }
+
+    constantConstructorArgument(model) {
+        if (!this.src)
+            return this;
+
+        const value = _.get(this.src, model);
+        const dflt = _.get(this.dflts, model);
+
+        if (nonNil(value) && nonNil(dflt) && value !== dflt.value)
+            this.arguments.push({clsName: dflt.clsName, constant: true, value});
+
+        return this;
+    }
+
+    propertyConstructorArgument(value, hint = '') {
+        this.arguments.push({clsName: 'PROPERTY', value, hint});
+
+        return this;
+    }
+
+    /**
+     * @param {String} id
+     * @param {EmptyBean|Bean} value
+     * @returns {Bean}
+     */
+    beanConstructorArgument(id, value) {
+        this.arguments.push({clsName: 'BEAN', id, value});
+
+        return this;
+    }
+
+    /**
+     * @param {String} id
+     * @param {String} model
+     * @param {Array.<Object>} entries
+     * @returns {Bean}
+     */
+    mapConstructorArgument(id, model, entries) {
+        if (!this.src)
+            return this;
+
+        const dflt = _.get(this.dflts, model);
+
+        if (nonEmpty(entries) && nonNil(dflt) && entries !== dflt.entries) {
+            this.arguments.push({
+                clsName: 'MAP',
+                id,
+                keyClsName: dflt.keyClsName,
+                keyField: dflt.keyField || 'name',
+                valClsName: dflt.valClsName,
+                valField: dflt.valField || 'value',
+                entries
+            });
+        }
+
+        return this;
+    }
+
+    valueOf(path) {
+        return _.get(this.src, path) || _.get(this.dflts, path + '.value') || _.get(this.dflts, path);
+    }
+
+    includes(...paths) {
+        return this.src && _.every(paths, (path) => {
+            const value = _.get(this.src, path);
+            const dflt = _.get(this.dflts, path);
+
+            return nonNil(value) && value !== dflt;
+        });
+    }
+
+    prop(clsName, name, value) {
+        this.properties.push({clsName, name, value});
+    }
+
+    boolProperty(model, name = model) {
+        return this._property(this.properties, 'boolean', model, name, nonNil);
+    }
+
+    byteProperty(model, name = model) {
+        return this._property(this.properties, 'byte', model, name, nonNil);
+    }
+
+    intProperty(model, name = model) {
+        return this._property(this.properties, 'int', model, name, nonNil);
+    }
+
+    longProperty(model, name = model) {
+        return this._property(this.properties, 'long', model, name, nonNil);
+    }
+
+    floatProperty(model, name = model) {
+        return this._property(this.properties, 'float', model, name, nonNil);
+    }
+
+    doubleProperty(model, name = model) {
+        return this._property(this.properties, 'double', model, name, nonNil);
+    }
+
+    property(name, value, hint) {
+        this.properties.push({clsName: 'PROPERTY', name, value, hint});
+
+        return this;
+    }
+
+    propertyChar(name, value, hint) {
+        this.properties.push({clsName: 'PROPERTY_CHAR', name, value, hint});
+
+        return this;
+    }
+
+    propertyInt(name, value, hint) {
+        this.properties.push({clsName: 'PROPERTY_INT', name, value, hint});
+
+        return this;
+    }
+
+    stringProperty(model, name = model, mapper) {
+        return this._property(this.properties, 'java.lang.String', model, name, nonEmpty, mapper);
+    }
+
+    pathProperty(model, name = model) {
+        return this._property(this.properties, 'PATH', model, name, nonEmpty);
+    }
+
+    pathArrayProperty(id, name, items, varArg) {
+        if (items && items.length)
+            this.properties.push({clsName: 'PATH_ARRAY', id, name, items, varArg, typeClsName: 'PATH'});
+
+        return this;
+    }
+
+    classProperty(model, name = model) {
+        return this._property(this.properties, 'java.lang.Class', model, name, nonEmpty);
+    }
+
+    enumProperty(model, name = model) {
+        if (!this.src)
+            return this;
+
+        const value = _.get(this.src, model);
+        const dflt = _.get(this.dflts, model);
+
+        if (nonNil(value) && nonNil(dflt) && value !== dflt.value)
+            this.properties.push({clsName: dflt.clsName, name, value: dflt.mapper ? dflt.mapper(value) : value});
+
+        return this;
+    }
+
+    emptyBeanProperty(model, name = model) {
+        if (!this.src)
+            return this;
+
+        const cls = _.get(this.src, model);
+        const dflt = _.get(this.dflts, model);
+
+        if (nonEmpty(cls) && cls !== dflt)
+            this.properties.push({clsName: 'BEAN', name, value: new EmptyBean(cls)});
+
+        return this;
+    }
+
+    /**
+     * @param {String} name
+     * @param {EmptyBean|Bean} value
+     * @returns {Bean}
+     */
+    beanProperty(name, value) {
+        this.properties.push({clsName: 'BEAN', name, value});
+
+        return this;
+    }
+
+    /**
+     * @param {String} id
+     * @param {String} name
+     * @param {Array} items
+     * @param {String} typeClsName
+     * @returns {Bean}
+     */
+    arrayProperty(id, name, items, typeClsName = 'java.lang.String') {
+        if (items && items.length)
+            this.properties.push({clsName: 'ARRAY', id, name, items, typeClsName});
+
+        return this;
+    }
+
+    /**
+     * @param {String} id
+     * @param {String} name
+     * @param {Array} items
+     * @param {String} typeClsName
+     * @returns {Bean}
+     */
+    varArgProperty(id, name, items, typeClsName = 'java.lang.String') {
+        if (items && items.length)
+            this.properties.push({clsName: 'ARRAY', id, name, items, typeClsName, varArg: true});
+
+        return this;
+    }
+
+    /**
+     * @param {String} id
+     * @param {String} name
+     * @param {Array} items
+     * @param {String} typeClsName
+     * @param {String} implClsName
+     * @returns {Bean}
+     */
+    collectionProperty(id, name, items, typeClsName = 'java.lang.String', implClsName = 'java.util.ArrayList') {
+        if (items && items.length)
+            this.properties.push({id, name, items, clsName: 'COLLECTION', typeClsName, implClsName});
+
+        return this;
+    }
+
+    /**
+     * @param {String} id
+     * @param {String} model
+     * @param {String} [name]
+     * @param {Boolean} [ordered]
+     * @returns {Bean}
+     */
+    mapProperty(id, model, name = model, ordered = false) {
+        if (!this.src)
+            return this;
+
+        const entries = _.isString(model) ? _.get(this.src, model) : model;
+        const dflt = _.isString(model) ? _.get(this.dflts, model) : _.get(this.dflts, name);
+
+        if (nonEmpty(entries) && nonNil(dflt) && entries !== dflt.entries) {
+            this.properties.push({
+                clsName: 'MAP',
+                id,
+                name,
+                ordered,
+                keyClsName: dflt.keyClsName,
+                keyField: dflt.keyField || 'name',
+                valClsName: dflt.valClsName,
+                valClsNameShow: dflt.valClsNameShow,
+                valField: dflt.valField || 'value',
+                entries,
+                keyClsGenericType: dflt.keyClsGenericType,
+                isKeyClsGenericTypeExtended: dflt.isKeyClsGenericTypeExtended
+            });
+        }
+
+        return this;
+    }
+
+    propsProperty(id, model, name = model) {
+        if (!this.src)
+            return this;
+
+        const entries = _.get(this.src, model);
+
+        if (nonEmpty(entries))
+            this.properties.push({clsName: 'java.util.Properties', id, name, entries});
+
+        return this;
+    }
+
+    /**
+     * @param {String} id
+     * @param {String} name
+     * @param {EmptyBean|Bean} value
+     */
+    dataSource(id, name, value) {
+        if (value)
+            this.properties.push({clsName: 'DATA_SOURCE', id, name, value});
+
+        return this;
+    }
+
+    /**
+     * @param {String} id
+     * @param {String} name
+     * @param {Array<String>} eventTypes
+     */
+    eventTypes(id, name, eventTypes) {
+        this.properties.push({clsName: 'EVENT_TYPES', id, name, eventTypes});
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/ConfigurationGenerator.js b/modules/frontend/app/configuration/generator/generator/ConfigurationGenerator.js
new file mode 100644
index 0000000..76d3118
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/ConfigurationGenerator.js
@@ -0,0 +1,2998 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import DFLT_DIALECTS from 'app/data/dialects.json';
+
+import {Bean, EmptyBean} from './Beans';
+
+import IgniteClusterDefaults from './defaults/Cluster.service';
+import IgniteEventGroups from './defaults/Event-groups.service';
+import IgniteCacheDefaults from './defaults/Cache.service';
+import IgniteIGFSDefaults from './defaults/IGFS.service';
+import ArtifactVersionChecker from './ArtifactVersionChecker.service';
+
+import JavaTypes from '../../../services/JavaTypes.service';
+import VersionService from 'app/services/Version.service';
+
+import _ from 'lodash';
+import isNil from 'lodash/isNil';
+import {nonEmpty, nonNil} from 'app/utils/lodashMixins';
+// Pom dependency information.
+import POM_DEPENDENCIES from 'app/data/pom-dependencies.json';
+
+const clusterDflts = new IgniteClusterDefaults();
+const cacheDflts = new IgniteCacheDefaults();
+const igfsDflts = new IgniteIGFSDefaults();
+const javaTypes = new JavaTypes(clusterDflts, cacheDflts, igfsDflts);
+const versionService = new VersionService();
+
+export default class IgniteConfigurationGenerator {
+    static eventGrps = new IgniteEventGroups();
+
+    static igniteConfigurationBean(cluster) {
+        return new Bean('org.apache.ignite.configuration.IgniteConfiguration', 'cfg', cluster, clusterDflts);
+    }
+
+    static igfsConfigurationBean(igfs) {
+        return new Bean('org.apache.ignite.configuration.FileSystemConfiguration', 'igfs', igfs, igfsDflts);
+    }
+
+    static cacheConfigurationBean(cache) {
+        return new Bean('org.apache.ignite.configuration.CacheConfiguration', 'ccfg', cache, cacheDflts);
+    }
+
+    static domainConfigurationBean(domain) {
+        return new Bean('org.apache.ignite.cache.QueryEntity', 'qryEntity', domain, cacheDflts);
+    }
+
+    static domainJdbcTypeBean(domain) {
+        return new Bean('org.apache.ignite.cache.store.jdbc.JdbcType', 'type', domain);
+    }
+
+    static discoveryConfigurationBean(discovery) {
+        return new Bean('org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi', 'discovery', discovery, clusterDflts.discovery);
+    }
+
+    /**
+     * Function to generate ignite configuration.
+     *
+     * @param {Object} cluster Cluster to process.
+     * @param {Object} targetVer Target version of configuration.
+     * @param {Boolean} client Is client configuration.
+     * @return {Bean} Generated ignite configuration.
+     */
+    static igniteConfiguration(cluster, targetVer, client) {
+        const available = versionService.since.bind(versionService, targetVer.ignite);
+
+        const cfg = this.igniteConfigurationBean(cluster);
+
+        this.clusterGeneral(cluster, available, cfg, client);
+        this.clusterAtomics(cluster.atomicConfiguration, available, cfg);
+        this.clusterBinary(cluster.binaryConfiguration, cfg);
+        this.clusterCacheKeyConfiguration(cluster.cacheKeyConfiguration, cfg);
+        this.clusterCheckpoint(cluster, available, cluster.caches, cfg);
+
+        if (available('2.3.0'))
+            this.clusterClientConnector(cluster, available, cfg);
+
+        this.clusterCollision(cluster.collision, cfg);
+        this.clusterCommunication(cluster, available, cfg);
+        this.clusterConnector(cluster.connector, cfg);
+
+        // Since ignite 2.3
+        if (available('2.3.0'))
+            this.clusterDataStorageConfiguration(cluster, available, cfg);
+
+        this.clusterDeployment(cluster, available, cfg);
+        this.clusterEncryption(cluster.encryptionSpi, available, cfg);
+        this.clusterEvents(cluster, available, cfg);
+        this.clusterFailover(cluster, available, cfg);
+        this.clusterHadoop(cluster.hadoopConfiguration, cfg);
+        this.clusterLoadBalancing(cluster, cfg);
+        this.clusterLogger(cluster.logger, cfg);
+        this.clusterMarshaller(cluster, available, cfg);
+
+        // Since ignite 2.0 and deprecated in ignite 2.3
+        if (available(['2.0.0', '2.3.0']))
+            this.clusterMemory(cluster.memoryConfiguration, available, cfg);
+
+        this.clusterMisc(cluster, available, cfg);
+        this.clusterMetrics(cluster, available, cfg);
+        this.clusterMvcc(cluster, available, cfg);
+        this.clusterODBC(cluster.odbc, available, cfg);
+
+        // Since ignite 2.1 deprecated in ignite 2.3
+        if (available(['2.1.0', '2.3.0']))
+            this.clusterPersistence(cluster.persistenceStoreConfiguration, available, cfg);
+
+        if (available(['2.1.0', '2.3.0']))
+            this.clusterQuery(cluster, available, cfg);
+
+        this.clusterServiceConfiguration(cluster.serviceConfigurations, cluster.caches, cfg);
+        this.clusterSsl(cluster, available, cfg);
+
+        // Deprecated in ignite 2.0
+        if (available(['1.0.0', '2.0.0']))
+            this.clusterSwap(cluster, cfg);
+
+        this.clusterPools(cluster, available, cfg);
+        this.clusterTime(cluster, available, cfg);
+        this.clusterTransactions(cluster.transactionConfiguration, available, cfg);
+        this.clusterUserAttributes(cluster, cfg);
+
+        this.clusterCaches(cluster, cluster.caches, cluster.igfss, available, client, cfg);
+
+        if (!client)
+            this.clusterIgfss(cluster.igfss, available, cfg);
+
+        return cfg;
+    }
+
+    static dialectClsName(dialect) {
+        return DFLT_DIALECTS[dialect] || 'Unknown database: ' + (dialect || 'Choose JDBC dialect');
+    }
+
+    static dataSourceBean(id, dialect, available, storeDeps, implementationVersion) {
+        let dsBean;
+
+        switch (dialect) {
+            case 'Generic':
+                dsBean = new Bean('com.mchange.v2.c3p0.ComboPooledDataSource', id, {})
+                    .property('jdbcUrl', `${id}.jdbc.url`, 'jdbc:your_database');
+
+                break;
+            case 'Oracle':
+                dsBean = new Bean('oracle.jdbc.pool.OracleDataSource', id, {})
+                    .property('URL', `${id}.jdbc.url`, 'jdbc:oracle:thin:@[host]:[port]:[database]');
+
+                break;
+            case 'DB2':
+                dsBean = new Bean('com.ibm.db2.jcc.DB2DataSource', id, {})
+                    .property('serverName', `${id}.jdbc.server_name`, 'YOUR_DATABASE_SERVER_NAME')
+                    .propertyInt('portNumber', `${id}.jdbc.port_number`, 'YOUR_JDBC_PORT_NUMBER')
+                    .property('databaseName', `${id}.jdbc.database_name`, 'YOUR_DATABASE_NAME')
+                    .propertyInt('driverType', `${id}.jdbc.driver_type`, 'YOUR_JDBC_DRIVER_TYPE');
+
+                break;
+            case 'SQLServer':
+                dsBean = new Bean('com.microsoft.sqlserver.jdbc.SQLServerDataSource', id, {})
+                    .property('URL', `${id}.jdbc.url`, 'jdbc:sqlserver://[host]:[port][;databaseName=database]');
+
+                break;
+            case 'MySQL':
+                const dep = storeDeps
+                    ? _.find(storeDeps, (d) => d.name === dialect)
+                    : _.first(ArtifactVersionChecker.latestVersions(this._getArtifact({dialect, implementationVersion}, available)));
+
+                const ver = parseInt(dep.version.split('.')[0], 10);
+
+                dsBean = new Bean(ver < 8 ? 'com.mysql.jdbc.jdbc2.optional.MysqlDataSource' : 'com.mysql.cj.jdbc.MysqlDataSource', id, {})
+                    .property('URL', `${id}.jdbc.url`, 'jdbc:mysql://[host]:[port]/[database]');
+
+                break;
+            case 'PostgreSQL':
+                dsBean = new Bean('org.postgresql.ds.PGPoolingDataSource', id, {})
+                    .property('url', `${id}.jdbc.url`, 'jdbc:postgresql://[host]:[port]/[database]');
+
+                break;
+            case 'H2':
+                dsBean = new Bean('org.h2.jdbcx.JdbcDataSource', id, {})
+                    .property('URL', `${id}.jdbc.url`, 'jdbc:h2:tcp://[host]/[database]');
+
+                break;
+            default:
+        }
+
+        if (dsBean) {
+            dsBean.property('user', `${id}.jdbc.username`, 'YOUR_USER_NAME')
+                .property('password', `${id}.jdbc.password`, 'YOUR_PASSWORD');
+        }
+
+        return dsBean;
+    }
+
+    // Generate general section.
+    static clusterGeneral(cluster, available, cfg = this.igniteConfigurationBean(cluster), client = false) {
+        if (client)
+            cfg.prop('boolean', 'clientMode', true);
+
+        if (available('2.0.0'))
+            cfg.stringProperty('name', 'igniteInstanceName');
+        else
+            cfg.stringProperty('name', 'gridName');
+
+        cfg.stringProperty('localHost');
+
+        if (isNil(cluster.discovery))
+            return cfg;
+
+        const discovery = IgniteConfigurationGenerator.discoveryConfigurationBean(cluster.discovery);
+
+        let ipFinder;
+
+        switch (discovery.valueOf('kind')) {
+            case 'Vm':
+                ipFinder = new Bean('org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder',
+                    'ipFinder', cluster.discovery.Vm, clusterDflts.discovery.Vm);
+
+                ipFinder.collectionProperty('addrs', 'addresses', cluster.discovery.Vm.addresses);
+
+                break;
+            case 'Multicast':
+                ipFinder = new Bean('org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder',
+                    'ipFinder', cluster.discovery.Multicast, clusterDflts.discovery.Multicast);
+
+                ipFinder.stringProperty('multicastGroup')
+                    .intProperty('multicastPort')
+                    .intProperty('responseWaitTime')
+                    .intProperty('addressRequestAttempts')
+                    .stringProperty('localAddress')
+                    .collectionProperty('addrs', 'addresses', cluster.discovery.Multicast.addresses);
+
+                break;
+            case 'S3':
+                ipFinder = new Bean('org.apache.ignite.spi.discovery.tcp.ipfinder.s3.TcpDiscoveryS3IpFinder',
+                    'ipFinder', cluster.discovery.S3, clusterDflts.discovery.S3);
+
+                ipFinder.stringProperty('bucketName');
+
+                if (available('2.4.0')) {
+                    ipFinder.stringProperty('bucketEndpoint')
+                        .stringProperty('SSEAlgorithm');
+                }
+
+                break;
+            case 'Cloud':
+                ipFinder = new Bean('org.apache.ignite.spi.discovery.tcp.ipfinder.cloud.TcpDiscoveryCloudIpFinder',
+                    'ipFinder', cluster.discovery.Cloud, clusterDflts.discovery.Cloud);
+
+                ipFinder.stringProperty('credential')
+                    .pathProperty('credentialPath')
+                    .stringProperty('identity')
+                    .stringProperty('provider')
+                    .collectionProperty('regions', 'regions', cluster.discovery.Cloud.regions)
+                    .collectionProperty('zones', 'zones', cluster.discovery.Cloud.zones);
+
+                break;
+            case 'GoogleStorage':
+                ipFinder = new Bean('org.apache.ignite.spi.discovery.tcp.ipfinder.gce.TcpDiscoveryGoogleStorageIpFinder',
+                    'ipFinder', cluster.discovery.GoogleStorage, clusterDflts.discovery.GoogleStorage);
+
+                ipFinder.stringProperty('projectName')
+                    .stringProperty('bucketName')
+                    .pathProperty('serviceAccountP12FilePath')
+                    .stringProperty('serviceAccountId');
+
+                break;
+            case 'Jdbc':
+                ipFinder = new Bean('org.apache.ignite.spi.discovery.tcp.ipfinder.jdbc.TcpDiscoveryJdbcIpFinder',
+                    'ipFinder', cluster.discovery.Jdbc, clusterDflts.discovery.Jdbc);
+
+                ipFinder.intProperty('initSchema');
+
+                if (ipFinder.includes('dataSourceBean', 'dialect')) {
+                    const id = ipFinder.valueOf('dataSourceBean');
+
+                    ipFinder.dataSource(id, 'dataSource', this.dataSourceBean(id, ipFinder.valueOf('dialect'), available));
+                }
+
+                break;
+            case 'SharedFs':
+                ipFinder = new Bean('org.apache.ignite.spi.discovery.tcp.ipfinder.sharedfs.TcpDiscoverySharedFsIpFinder',
+                    'ipFinder', cluster.discovery.SharedFs, clusterDflts.discovery.SharedFs);
+
+                ipFinder.pathProperty('path');
+
+                break;
+            case 'ZooKeeper':
+                const src = cluster.discovery.ZooKeeper;
+                const dflt = clusterDflts.discovery.ZooKeeper;
+
+                ipFinder = new Bean('org.apache.ignite.spi.discovery.tcp.ipfinder.zk.TcpDiscoveryZookeeperIpFinder',
+                    'ipFinder', src, dflt);
+
+                ipFinder.emptyBeanProperty('curator')
+                    .stringProperty('zkConnectionString');
+
+                const kind = _.get(src, 'retryPolicy.kind');
+
+                if (kind) {
+                    const policy = src.retryPolicy;
+
+                    let retryPolicyBean;
+
+                    switch (kind) {
+                        case 'ExponentialBackoff':
+                            retryPolicyBean = new Bean('org.apache.curator.retry.ExponentialBackoffRetry', null,
+                                policy.ExponentialBackoff, dflt.ExponentialBackoff)
+                                .intConstructorArgument('baseSleepTimeMs')
+                                .intConstructorArgument('maxRetries')
+                                .intConstructorArgument('maxSleepMs');
+
+                            break;
+                        case 'BoundedExponentialBackoff':
+                            retryPolicyBean = new Bean('org.apache.curator.retry.BoundedExponentialBackoffRetry',
+                                null, policy.BoundedExponentialBackoff, dflt.BoundedExponentialBackoffRetry)
+                                .intConstructorArgument('baseSleepTimeMs')
+                                .intConstructorArgument('maxSleepTimeMs')
+                                .intConstructorArgument('maxRetries');
+
+                            break;
+                        case 'UntilElapsed':
+                            retryPolicyBean = new Bean('org.apache.curator.retry.RetryUntilElapsed', null,
+                                policy.UntilElapsed, dflt.UntilElapsed)
+                                .intConstructorArgument('maxElapsedTimeMs')
+                                .intConstructorArgument('sleepMsBetweenRetries');
+
+                            break;
+
+                        case 'NTimes':
+                            retryPolicyBean = new Bean('org.apache.curator.retry.RetryNTimes', null,
+                                policy.NTimes, dflt.NTimes)
+                                .intConstructorArgument('n')
+                                .intConstructorArgument('sleepMsBetweenRetries');
+
+                            break;
+                        case 'OneTime':
+                            retryPolicyBean = new Bean('org.apache.curator.retry.RetryOneTime', null,
+                                policy.OneTime, dflt.OneTime)
+                                .intConstructorArgument('sleepMsBetweenRetry');
+
+                            break;
+                        case 'Forever':
+                            retryPolicyBean = new Bean('org.apache.curator.retry.RetryForever', null,
+                                policy.Forever, dflt.Forever)
+                                .intConstructorArgument('retryIntervalMs');
+
+                            break;
+                        case 'Custom':
+                            const className = _.get(policy, 'Custom.className');
+
+                            if (nonEmpty(className))
+                                retryPolicyBean = new EmptyBean(className);
+
+                            break;
+                        default:
+                            // No-op.
+                    }
+
+                    if (retryPolicyBean)
+                        ipFinder.beanProperty('retryPolicy', retryPolicyBean);
+                }
+
+                ipFinder.pathProperty('basePath')
+                    .stringProperty('serviceName')
+                    .boolProperty('allowDuplicateRegistrations');
+
+                break;
+
+            case 'Kubernetes':
+                ipFinder = new Bean('org.apache.ignite.spi.discovery.tcp.ipfinder.kubernetes.TcpDiscoveryKubernetesIpFinder',
+                    'ipFinder', cluster.discovery.Kubernetes, clusterDflts.discovery.Kubernetes);
+
+                ipFinder.stringProperty('serviceName')
+                    .stringProperty('namespace')
+                    .stringProperty('masterUrl')
+                    .pathProperty('accountToken');
+
+                break;
+
+            default:
+                // No-op.
+        }
+
+        if (ipFinder)
+            discovery.beanProperty('ipFinder', ipFinder);
+
+        this.clusterDiscovery(cluster.discovery, available, cfg, discovery);
+
+        return cfg;
+    }
+
+    static igfsDataCache(igfs, available) {
+        return this.cacheConfiguration({
+            name: igfs.name + '-data',
+            cacheMode: 'PARTITIONED',
+            atomicityMode: 'TRANSACTIONAL',
+            writeSynchronizationMode: 'FULL_SYNC',
+            backups: 0,
+            igfsAffinnityGroupSize: igfs.affinnityGroupSize || 512
+        }, available);
+    }
+
+    static igfsMetaCache(igfs, available) {
+        return this.cacheConfiguration({
+            name: igfs.name + '-meta',
+            cacheMode: 'REPLICATED',
+            atomicityMode: 'TRANSACTIONAL',
+            writeSynchronizationMode: 'FULL_SYNC'
+        }, available);
+    }
+
+    /**
+     * Get dependency artifact for specified datasource.
+     *
+     * @param source Datasource.
+     * @param available Function to check version availability.
+     * @return {Array<{{name: String, version: String}}>} Array of accordance datasource artifacts.
+     */
+    static _getArtifact(source, available) {
+        const deps = _.get(POM_DEPENDENCIES, source.dialect);
+
+        if (!deps)
+            return [];
+
+        const extractVersion = (version) => {
+            return _.isArray(version) ? _.find(version, (v) => available(v.range)).version : version;
+        };
+
+        return _.map(_.castArray(deps), ({version}) => {
+            return ({
+                name: source.dialect,
+                version: source.implementationVersion || extractVersion(version)
+            });
+        });
+    }
+
+    static clusterCaches(cluster, caches, igfss, available, client, cfg = this.igniteConfigurationBean(cluster)) {
+        const usedDataSourceVersions = [];
+
+        if (cluster.discovery.kind === 'Jdbc')
+            usedDataSourceVersions.push(...this._getArtifact(cluster.discovery.Jdbc, available));
+
+        _.forEach(cluster.checkpointSpi, (spi) => {
+            if (spi.kind === 'JDBC')
+                usedDataSourceVersions.push(...this._getArtifact(spi.JDBC, available));
+        });
+
+        _.forEach(caches, (cache) => {
+            if (_.get(cache, 'cacheStoreFactory.kind'))
+                usedDataSourceVersions.push(...this._getArtifact(cache.cacheStoreFactory[cache.cacheStoreFactory.kind], available));
+        });
+
+        const useDeps = _.uniqWith(ArtifactVersionChecker.latestVersions(usedDataSourceVersions), _.isEqual);
+
+        const ccfgs = _.map(caches, (cache) => this.cacheConfiguration(cache, available, useDeps));
+
+        if (!client) {
+            _.forEach(igfss, (igfs) => {
+                ccfgs.push(this.igfsDataCache(igfs, available));
+                ccfgs.push(this.igfsMetaCache(igfs, available));
+            });
+        }
+
+        cfg.varArgProperty('ccfgs', 'cacheConfiguration', ccfgs, 'org.apache.ignite.configuration.CacheConfiguration');
+
+        return cfg;
+    }
+
+    // Generate atomics group.
+    static clusterAtomics(atomics, available, cfg = this.igniteConfigurationBean()) {
+        const available2_1 = available('2.1.0');
+
+        const acfg = new Bean('org.apache.ignite.configuration.AtomicConfiguration', 'atomicCfg',
+            atomics, clusterDflts.atomics);
+
+        acfg.enumProperty('cacheMode')
+            .intProperty('atomicSequenceReserveSize');
+
+        if (acfg.valueOf('cacheMode') === 'PARTITIONED')
+            acfg.intProperty('backups');
+
+        if (available2_1 && nonNil(atomics))
+            this.affinity(atomics.affinity, acfg);
+
+        if (available2_1)
+            acfg.stringProperty('groupName');
+
+        if (acfg.isEmpty())
+            return cfg;
+
+        cfg.beanProperty('atomicConfiguration', acfg);
+
+        return cfg;
+    }
+
+    // Generate binary group.
+    static clusterBinary(binary, cfg = this.igniteConfigurationBean()) {
+        const binaryCfg = new Bean('org.apache.ignite.configuration.BinaryConfiguration', 'binaryCfg',
+            binary, clusterDflts.binary);
+
+        binaryCfg.emptyBeanProperty('idMapper')
+            .emptyBeanProperty('nameMapper')
+            .emptyBeanProperty('serializer');
+
+        const typeCfgs = [];
+
+        _.forEach(binary.typeConfigurations, (type) => {
+            const typeCfg = new Bean('org.apache.ignite.binary.BinaryTypeConfiguration',
+                javaTypes.toJavaName('binaryType', type.typeName), type, clusterDflts.binary.typeConfigurations);
+
+            typeCfg.stringProperty('typeName')
+                .emptyBeanProperty('idMapper')
+                .emptyBeanProperty('nameMapper')
+                .emptyBeanProperty('serializer')
+                .boolProperty('enum')
+                .mapProperty('enumValues', _.map(type.enumValues, (v, idx) => ({name: v, value: idx})), 'enumValues');
+
+            if (typeCfg.nonEmpty())
+                typeCfgs.push(typeCfg);
+        });
+
+        binaryCfg.collectionProperty('types', 'typeConfigurations', typeCfgs, 'org.apache.ignite.binary.BinaryTypeConfiguration')
+            .boolProperty('compactFooter');
+
+        if (binaryCfg.isEmpty())
+            return cfg;
+
+        cfg.beanProperty('binaryConfiguration', binaryCfg);
+
+        return cfg;
+    }
+
+    // Generate cache key configurations.
+    static clusterCacheKeyConfiguration(keyCfgs, cfg = this.igniteConfigurationBean()) {
+        const items = _.reduce(keyCfgs, (acc, keyCfg) => {
+            if (keyCfg.typeName && keyCfg.affinityKeyFieldName) {
+                acc.push(new Bean('org.apache.ignite.cache.CacheKeyConfiguration', null, keyCfg)
+                    .stringConstructorArgument('typeName')
+                    .stringConstructorArgument('affinityKeyFieldName'));
+            }
+
+            return acc;
+        }, []);
+
+        if (_.isEmpty(items))
+            return cfg;
+
+        cfg.arrayProperty('cacheKeyConfiguration', 'cacheKeyConfiguration', items,
+            'org.apache.ignite.cache.CacheKeyConfiguration');
+
+        return cfg;
+    }
+
+    // Generate checkpoint configurations.
+    static clusterCheckpoint(cluster, available, caches, cfg = this.igniteConfigurationBean()) {
+        const cfgs = _.filter(_.map(cluster.checkpointSpi, (spi) => {
+            switch (_.get(spi, 'kind')) {
+                case 'FS':
+                    const fsBean = new Bean('org.apache.ignite.spi.checkpoint.sharedfs.SharedFsCheckpointSpi',
+                        'checkpointSpiFs', spi.FS);
+
+                    fsBean.collectionProperty('directoryPaths', 'directoryPaths', _.get(spi, 'FS.directoryPaths'))
+                        .emptyBeanProperty('checkpointListener');
+
+                    return fsBean;
+
+                case 'Cache':
+                    const cacheBean = new Bean('org.apache.ignite.spi.checkpoint.cache.CacheCheckpointSpi',
+                        'checkpointSpiCache', spi.Cache);
+
+                    const curCache = _.get(spi, 'Cache.cache');
+
+                    const cache = _.find(caches, (c) => curCache && (c._id === curCache || _.get(c, 'cache._id') === curCache));
+
+                    if (cache)
+                        cacheBean.prop('java.lang.String', 'cacheName', cache.name || cache.cache.name);
+
+                    cacheBean.stringProperty('cacheName')
+                        .emptyBeanProperty('checkpointListener');
+
+                    return cacheBean;
+
+                case 'S3':
+                    const s3Bean = new Bean('org.apache.ignite.spi.checkpoint.s3.S3CheckpointSpi',
+                        'checkpointSpiS3', spi.S3, clusterDflts.checkpointSpi.S3);
+
+                    let credentialsBean = null;
+
+                    switch (_.get(spi.S3, 'awsCredentials.kind')) {
+                        case 'Basic':
+                            credentialsBean = new Bean('com.amazonaws.auth.BasicAWSCredentials', 'awsCredentials', {});
+
+                            credentialsBean.propertyConstructorArgument('checkpoint.s3.credentials.accessKey', 'YOUR_S3_ACCESS_KEY')
+                                .propertyConstructorArgument('checkpoint.s3.credentials.secretKey', 'YOUR_S3_SECRET_KEY');
+
+                            break;
+
+                        case 'Properties':
+                            credentialsBean = new Bean('com.amazonaws.auth.PropertiesCredentials', 'awsCredentials', {});
+
+                            const fileBean = new Bean('java.io.File', '', spi.S3.awsCredentials.Properties)
+                                .pathConstructorArgument('path');
+
+                            if (fileBean.nonEmpty())
+                                credentialsBean.beanConstructorArgument('file', fileBean);
+
+                            break;
+
+                        case 'Anonymous':
+                            credentialsBean = new Bean('com.amazonaws.auth.AnonymousAWSCredentials', 'awsCredentials', {});
+
+                            break;
+
+                        case 'BasicSession':
+                            credentialsBean = new Bean('com.amazonaws.auth.BasicSessionCredentials', 'awsCredentials', {});
+
+                            // TODO 2054 Arguments in one line is very long string.
+                            credentialsBean.propertyConstructorArgument('checkpoint.s3.credentials.accessKey')
+                                .propertyConstructorArgument('checkpoint.s3.credentials.secretKey')
+                                .propertyConstructorArgument('checkpoint.s3.credentials.sessionToken');
+
+                            break;
+
+                        case 'Custom':
+                            const className = _.get(spi.S3.awsCredentials, 'Custom.className');
+
+                            if (className)
+                                credentialsBean = new Bean(className, 'awsCredentials', {});
+
+                            break;
+
+                        default:
+                            break;
+                    }
+
+                    if (credentialsBean)
+                        s3Bean.beanProperty('awsCredentials', credentialsBean);
+
+                    s3Bean.stringProperty('bucketNameSuffix');
+
+                    if (available('2.4.0')) {
+                        s3Bean.stringProperty('bucketEndpoint')
+                            .stringProperty('SSEAlgorithm');
+                    }
+
+                    const clientBean = new Bean('com.amazonaws.ClientConfiguration', 'clientCfg', spi.S3.clientConfiguration,
+                        clusterDflts.checkpointSpi.S3.clientConfiguration);
+
+                    clientBean.enumProperty('protocol')
+                        .intProperty('maxConnections')
+                        .stringProperty('userAgentPrefix')
+                        .stringProperty('userAgentSuffix');
+
+                    const locAddr = new Bean('java.net.InetAddress', '', spi.S3.clientConfiguration)
+                        .factoryMethod('getByName')
+                        .stringConstructorArgument('localAddress');
+
+                    if (locAddr.nonEmpty())
+                        clientBean.beanProperty('localAddress', locAddr);
+
+                    clientBean.stringProperty('proxyHost')
+                        .intProperty('proxyPort')
+                        .stringProperty('proxyUsername');
+
+                    const userName = clientBean.valueOf('proxyUsername');
+
+                    if (userName)
+                        clientBean.property('proxyPassword', `checkpoint.s3.proxy.${userName}.password`);
+
+                    clientBean.stringProperty('proxyDomain')
+                        .stringProperty('proxyWorkstation')
+                        .stringProperty('nonProxyHosts');
+
+                    const retryPolicy = spi.S3.clientConfiguration.retryPolicy;
+
+                    if (retryPolicy) {
+                        const kind = retryPolicy.kind;
+
+                        const policy = retryPolicy[kind];
+
+                        let retryBean;
+
+                        switch (kind) {
+                            case 'Default':
+                                retryBean = new Bean('com.amazonaws.retry.RetryPolicy', 'retryPolicy', {
+                                    retryCondition: 'DEFAULT_RETRY_CONDITION',
+                                    backoffStrategy: 'DEFAULT_BACKOFF_STRATEGY',
+                                    maxErrorRetry: 'DEFAULT_MAX_ERROR_RETRY',
+                                    honorMaxErrorRetryInClientConfig: true
+                                }, clusterDflts.checkpointSpi.S3.clientConfiguration.retryPolicy);
+
+                                retryBean.constantConstructorArgument('retryCondition')
+                                    .constantConstructorArgument('backoffStrategy')
+                                    .constantConstructorArgument('maxErrorRetry')
+                                    .constructorArgument('java.lang.Boolean', retryBean.valueOf('honorMaxErrorRetryInClientConfig'));
+
+                                break;
+
+                            case 'DefaultMaxRetries':
+                                retryBean = new Bean('com.amazonaws.retry.RetryPolicy', 'retryPolicy', {
+                                    retryCondition: 'DEFAULT_RETRY_CONDITION',
+                                    backoffStrategy: 'DEFAULT_BACKOFF_STRATEGY',
+                                    maxErrorRetry: _.get(policy, 'maxErrorRetry') || -1,
+                                    honorMaxErrorRetryInClientConfig: false
+                                }, clusterDflts.checkpointSpi.S3.clientConfiguration.retryPolicy);
+
+                                retryBean.constantConstructorArgument('retryCondition')
+                                    .constantConstructorArgument('backoffStrategy')
+                                    .constructorArgument('java.lang.Integer', retryBean.valueOf('maxErrorRetry'))
+                                    .constructorArgument('java.lang.Boolean', retryBean.valueOf('honorMaxErrorRetryInClientConfig'));
+
+                                break;
+
+                            case 'DynamoDB':
+                                retryBean = new Bean('com.amazonaws.retry.RetryPolicy', 'retryPolicy', {
+                                    retryCondition: 'DEFAULT_RETRY_CONDITION',
+                                    backoffStrategy: 'DYNAMODB_DEFAULT_BACKOFF_STRATEGY',
+                                    maxErrorRetry: 'DYNAMODB_DEFAULT_MAX_ERROR_RETRY',
+                                    honorMaxErrorRetryInClientConfig: true
+                                }, clusterDflts.checkpointSpi.S3.clientConfiguration.retryPolicy);
+
+                                retryBean.constantConstructorArgument('retryCondition')
+                                    .constantConstructorArgument('backoffStrategy')
+                                    .constantConstructorArgument('maxErrorRetry')
+                                    .constructorArgument('java.lang.Boolean', retryBean.valueOf('honorMaxErrorRetryInClientConfig'));
+
+                                break;
+
+                            case 'DynamoDBMaxRetries':
+                                retryBean = new Bean('com.amazonaws.retry.RetryPolicy', 'retryPolicy', {
+                                    retryCondition: 'DEFAULT_RETRY_CONDITION',
+                                    backoffStrategy: 'DYNAMODB_DEFAULT_BACKOFF_STRATEGY',
+                                    maxErrorRetry: _.get(policy, 'maxErrorRetry') || -1,
+                                    honorMaxErrorRetryInClientConfig: false
+                                }, clusterDflts.checkpointSpi.S3.clientConfiguration.retryPolicy);
+
+                                retryBean.constantConstructorArgument('retryCondition')
+                                    .constantConstructorArgument('backoffStrategy')
+                                    .constructorArgument('java.lang.Integer', retryBean.valueOf('maxErrorRetry'))
+                                    .constructorArgument('java.lang.Boolean', retryBean.valueOf('honorMaxErrorRetryInClientConfig'));
+
+                                break;
+
+                            case 'Custom':
+                                retryBean = new Bean('com.amazonaws.retry.RetryPolicy', 'retryPolicy', policy,
+                                    clusterDflts.checkpointSpi.S3.clientConfiguration.retryPolicy);
+
+                                retryBean.beanConstructorArgument('retryCondition', retryBean.valueOf('retryCondition') ? new EmptyBean(retryBean.valueOf('retryCondition')) : null)
+                                    .beanConstructorArgument('backoffStrategy', retryBean.valueOf('backoffStrategy') ? new EmptyBean(retryBean.valueOf('backoffStrategy')) : null)
+                                    .constructorArgument('java.lang.Integer', retryBean.valueOf('maxErrorRetry'))
+                                    .constructorArgument('java.lang.Boolean', retryBean.valueOf('honorMaxErrorRetryInClientConfig'));
+
+                                break;
+
+                            default:
+                                break;
+                        }
+
+                        if (retryBean)
+                            clientBean.beanProperty('retryPolicy', retryBean);
+                    }
+
+                    clientBean.intProperty('maxErrorRetry')
+                        .intProperty('socketTimeout')
+                        .intProperty('connectionTimeout')
+                        .intProperty('requestTimeout')
+                        .stringProperty('signerOverride')
+                        .longProperty('connectionTTL')
+                        .longProperty('connectionMaxIdleMillis')
+                        .emptyBeanProperty('dnsResolver')
+                        .intProperty('responseMetadataCacheSize')
+                        .emptyBeanProperty('secureRandom')
+                        .intProperty('clientExecutionTimeout')
+                        .boolProperty('useReaper')
+                        .boolProperty('cacheResponseMetadata')
+                        .boolProperty('useExpectContinue')
+                        .boolProperty('useThrottleRetries')
+                        .boolProperty('useGzip')
+                        .boolProperty('preemptiveBasicProxyAuth')
+                        .boolProperty('useTcpKeepAlive');
+
+                    if (clientBean.nonEmpty())
+                        s3Bean.beanProperty('clientConfiguration', clientBean);
+
+                    s3Bean.emptyBeanProperty('checkpointListener');
+
+                    return s3Bean;
+
+                case 'JDBC':
+                    const jdbcBean = new Bean('org.apache.ignite.spi.checkpoint.jdbc.JdbcCheckpointSpi',
+                        'checkpointSpiJdbc', spi.JDBC, clusterDflts.checkpointSpi.JDBC);
+
+                    const id = jdbcBean.valueOf('dataSourceBean');
+                    const dialect = _.get(spi.JDBC, 'dialect');
+
+                    jdbcBean.dataSource(id, 'dataSource', this.dataSourceBean(id, dialect, available));
+
+                    if (!_.isEmpty(jdbcBean.valueOf('user'))) {
+                        jdbcBean.stringProperty('user')
+                            .property('pwd', `checkpoint.${jdbcBean.valueOf('dataSourceBean')}.${jdbcBean.valueOf('user')}.jdbc.password`, 'YOUR_PASSWORD');
+                    }
+
+                    jdbcBean.stringProperty('checkpointTableName')
+                        .stringProperty('keyFieldName')
+                        .stringProperty('keyFieldType')
+                        .stringProperty('valueFieldName')
+                        .stringProperty('valueFieldType')
+                        .stringProperty('expireDateFieldName')
+                        .stringProperty('expireDateFieldType')
+                        .intProperty('numberOfRetries')
+                        .emptyBeanProperty('checkpointListener');
+
+                    return jdbcBean;
+
+                case 'Custom':
+                    const clsName = _.get(spi, 'Custom.className');
+
+                    if (clsName)
+                        return new Bean(clsName, 'checkpointSpiCustom', spi.Cache);
+
+                    return null;
+
+                default:
+                    return null;
+            }
+        }), (checkpointBean) => nonNil(checkpointBean));
+
+        cfg.arrayProperty('checkpointSpi', 'checkpointSpi', cfgs, 'org.apache.ignite.spi.checkpoint.CheckpointSpi');
+
+        return cfg;
+    }
+
+    // Generate cluster query group.
+    static clusterClientConnector(cluster, available, cfg = this.igniteConfigurationBean(cluster)) {
+        if (!available('2.3.0'))
+            return cfg;
+
+        cfg.longProperty('longQueryWarningTimeout');
+
+        if (_.get(cluster, 'clientConnectorConfiguration.enabled') !== true)
+            return cfg;
+
+        const bean = new Bean('org.apache.ignite.configuration.ClientConnectorConfiguration', 'cliConnCfg',
+            cluster.clientConnectorConfiguration, clusterDflts.clientConnectorConfiguration);
+
+        bean.stringProperty('host')
+            .intProperty('port')
+            .intProperty('portRange')
+            .intProperty('socketSendBufferSize')
+            .intProperty('socketReceiveBufferSize')
+            .intProperty('maxOpenCursorsPerConnection')
+            .intProperty('threadPoolSize')
+            .boolProperty('tcpNoDelay');
+
+        if (available('2.4.0')) {
+            bean.longProperty('idleTimeout')
+                .boolProperty('jdbcEnabled')
+                .boolProperty('odbcEnabled')
+                .boolProperty('thinClientEnabled');
+        }
+
+        if (available('2.5.0')) {
+            bean.boolProperty('sslEnabled')
+                .boolProperty('sslClientAuth')
+                .boolProperty('useIgniteSslContextFactory')
+                .emptyBeanProperty('sslContextFactory');
+        }
+
+        cfg.beanProperty('clientConnectorConfiguration', bean);
+
+        return cfg;
+    }
+
+    // Generate collision group.
+    static clusterCollision(collision, cfg = this.igniteConfigurationBean()) {
+        let colSpi;
+
+        switch (_.get(collision, 'kind')) {
+            case 'JobStealing':
+                colSpi = new Bean('org.apache.ignite.spi.collision.jobstealing.JobStealingCollisionSpi',
+                    'colSpi', collision.JobStealing, clusterDflts.collision.JobStealing);
+
+                colSpi.intProperty('activeJobsThreshold')
+                    .intProperty('waitJobsThreshold')
+                    .longProperty('messageExpireTime')
+                    .intProperty('maximumStealingAttempts')
+                    .boolProperty('stealingEnabled')
+                    .emptyBeanProperty('externalCollisionListener')
+                    .mapProperty('stealingAttrs', 'stealingAttributes');
+
+                break;
+            case 'FifoQueue':
+                colSpi = new Bean('org.apache.ignite.spi.collision.fifoqueue.FifoQueueCollisionSpi',
+                    'colSpi', collision.FifoQueue, clusterDflts.collision.FifoQueue);
+
+                colSpi.intProperty('parallelJobsNumber')
+                    .intProperty('waitingJobsNumber');
+
+                break;
+            case 'PriorityQueue':
+                colSpi = new Bean('org.apache.ignite.spi.collision.priorityqueue.PriorityQueueCollisionSpi',
+                    'colSpi', collision.PriorityQueue, clusterDflts.collision.PriorityQueue);
+
+                colSpi.intProperty('parallelJobsNumber')
+                    .intProperty('waitingJobsNumber')
+                    .stringProperty('priorityAttributeKey')
+                    .stringProperty('jobPriorityAttributeKey')
+                    .intProperty('defaultPriority')
+                    .intProperty('starvationIncrement')
+                    .boolProperty('starvationPreventionEnabled');
+
+                break;
+            case 'Custom':
+                if (nonNil(_.get(collision, 'Custom.class')))
+                    colSpi = new EmptyBean(collision.Custom.class);
+
+                break;
+            default:
+                return cfg;
+        }
+
+        if (nonNil(colSpi))
+            cfg.beanProperty('collisionSpi', colSpi);
+
+        return cfg;
+    }
+
+    // Generate communication group.
+    static clusterCommunication(cluster, available, cfg = this.igniteConfigurationBean(cluster)) {
+        const commSpi = new Bean('org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi', 'communicationSpi',
+            cluster.communication, clusterDflts.communication);
+
+        commSpi.emptyBeanProperty('listener')
+            .stringProperty('localAddress')
+            .intProperty('localPort')
+            .intProperty('localPortRange')
+            .intProperty('sharedMemoryPort')
+            .intProperty('directBuffer')
+            .intProperty('directSendBuffer')
+            .longProperty('idleConnectionTimeout')
+            .longProperty('connectTimeout')
+            .longProperty('maxConnectTimeout')
+            .intProperty('reconnectCount')
+            .intProperty('socketSendBuffer')
+            .intProperty('socketReceiveBuffer')
+            .intProperty('messageQueueLimit')
+            .intProperty('slowClientQueueLimit')
+            .intProperty('tcpNoDelay')
+            .intProperty('ackSendThreshold')
+            .intProperty('unacknowledgedMessagesBufferSize')
+            .longProperty('socketWriteTimeout')
+            .intProperty('selectorsCount')
+            .longProperty('selectorSpins')
+            .intProperty('connectionsPerNode')
+            .emptyBeanProperty('addressResolver')
+            .boolProperty('usePairedConnections');
+
+        if (available('2.3.0'))
+            commSpi.boolProperty('filterReachableAddresses');
+
+        if (commSpi.nonEmpty())
+            cfg.beanProperty('communicationSpi', commSpi);
+
+        cfg.longProperty('networkTimeout')
+            .longProperty('networkSendRetryDelay')
+            .intProperty('networkSendRetryCount');
+
+        if (available('2.8.0'))
+            cfg.intProperty('networkCompressionLevel');
+
+        if (available('2.5.0'))
+            cfg.emptyBeanProperty('communicationFailureResolver');
+
+        if (available(['1.0.0', '2.3.0']))
+            cfg.longProperty('discoveryStartupDelay');
+
+        return cfg;
+    }
+
+    // Generate REST access configuration.
+    static clusterConnector(connector, cfg = this.igniteConfigurationBean()) {
+        const connCfg = new Bean('org.apache.ignite.configuration.ConnectorConfiguration',
+            'connectorConfiguration', connector, clusterDflts.connector);
+
+        if (connCfg.valueOf('enabled')) {
+            connCfg.pathProperty('jettyPath')
+                .stringProperty('host')
+                .intProperty('port')
+                .intProperty('portRange')
+                .longProperty('idleTimeout')
+                .longProperty('idleQueryCursorTimeout')
+                .longProperty('idleQueryCursorCheckFrequency')
+                .intProperty('receiveBufferSize')
+                .intProperty('sendBufferSize')
+                .intProperty('sendQueueLimit')
+                .intProperty('directBuffer')
+                .intProperty('noDelay')
+                .intProperty('selectorCount')
+                .intProperty('threadPoolSize')
+                .emptyBeanProperty('messageInterceptor')
+                .stringProperty('secretKey');
+
+            if (connCfg.valueOf('sslEnabled')) {
+                connCfg.intProperty('sslClientAuth')
+                    .emptyBeanProperty('sslFactory');
+            }
+
+            if (connCfg.nonEmpty())
+                cfg.beanProperty('connectorConfiguration', connCfg);
+        }
+
+        return cfg;
+    }
+
+    // Generate deployment group.
+    static clusterDeployment(cluster, available, cfg = this.igniteConfigurationBean(cluster)) {
+        cfg.enumProperty('deploymentMode')
+            .boolProperty('peerClassLoadingEnabled');
+
+        if (cfg.valueOf('peerClassLoadingEnabled')) {
+            cfg.intProperty('peerClassLoadingMissedResourcesCacheSize')
+                .intProperty('peerClassLoadingThreadPoolSize')
+                .varArgProperty('p2pLocClsPathExcl', 'peerClassLoadingLocalClassPathExclude',
+                    cluster.peerClassLoadingLocalClassPathExclude);
+        }
+
+        // Since ignite 2.0
+        if (available('2.0.0'))
+            cfg.emptyBeanProperty('classLoader');
+
+        let deploymentBean = null;
+
+        switch (_.get(cluster, 'deploymentSpi.kind')) {
+            case 'URI':
+                const uriDeployment = cluster.deploymentSpi.URI;
+
+                deploymentBean = new Bean('org.apache.ignite.spi.deployment.uri.UriDeploymentSpi', 'deploymentSpi', uriDeployment);
+
+                const scanners = _.map(uriDeployment.scanners, (scanner) => new EmptyBean(scanner));
+
+                deploymentBean.collectionProperty('uriList', 'uriList', uriDeployment.uriList)
+                    .stringProperty('temporaryDirectoryPath')
+                    .varArgProperty('scanners', 'scanners', scanners,
+                        'org.apache.ignite.spi.deployment.uri.scanners.UriDeploymentScanner')
+                    .emptyBeanProperty('listener')
+                    .boolProperty('checkMd5')
+                    .boolProperty('encodeUri');
+
+                cfg.beanProperty('deploymentSpi', deploymentBean);
+
+                break;
+
+            case 'Local':
+                deploymentBean = new Bean('org.apache.ignite.spi.deployment.local.LocalDeploymentSpi', 'deploymentSpi', cluster.deploymentSpi.Local);
+
+                deploymentBean.emptyBeanProperty('listener');
+
+                cfg.beanProperty('deploymentSpi', deploymentBean);
+
+                break;
+
+            case 'Custom':
+                cfg.emptyBeanProperty('deploymentSpi.Custom.className');
+
+                break;
+
+            default:
+                // No-op.
+        }
+
+        return cfg;
+    }
+
+    // Generate discovery group.
+    static clusterDiscovery(discovery, available, cfg = this.igniteConfigurationBean(), discoSpi = this.discoveryConfigurationBean(discovery)) {
+        discoSpi.stringProperty('localAddress')
+            .intProperty('localPort')
+            .intProperty('localPortRange')
+            .emptyBeanProperty('addressResolver')
+            .longProperty('socketTimeout')
+            .longProperty('ackTimeout')
+            .longProperty('maxAckTimeout')
+            .longProperty('networkTimeout')
+            .longProperty('joinTimeout')
+            .intProperty('threadPriority');
+
+        // Removed in ignite 2.0
+        if (available(['1.0.0', '2.0.0'])) {
+            discoSpi.intProperty('heartbeatFrequency')
+                .intProperty('maxMissedHeartbeats')
+                .intProperty('maxMissedClientHeartbeats');
+        }
+
+        discoSpi.longProperty('topHistorySize')
+            .emptyBeanProperty('listener')
+            .emptyBeanProperty('dataExchange')
+            .emptyBeanProperty('metricsProvider')
+            .intProperty('reconnectCount')
+            .longProperty('statisticsPrintFrequency')
+            .longProperty('ipFinderCleanFrequency')
+            .emptyBeanProperty('authenticator');
+
+        if (available('2.4.0'))
+            discoSpi.longProperty('reconnectDelay');
+
+        if (available('2.7.0'))
+            discoSpi.longProperty('connectionRecoveryTimeout');
+
+        if (available('2.8.0'))
+            discoSpi.intProperty('soLinger');
+
+        discoSpi.intProperty('forceServerMode')
+            .intProperty('clientReconnectDisabled');
+
+        if (discoSpi.nonEmpty())
+            cfg.beanProperty('discoverySpi', discoSpi);
+
+        return discoSpi;
+    }
+
+    // Execute event filtration in accordance to generated project version.
+    static filterEvents(eventGrps, available) {
+        if (available('2.0.0')) {
+            return _.reduce(eventGrps, (acc, eventGrp) => {
+                switch (eventGrp.value) {
+                    case 'EVTS_SWAPSPACE':
+                        // Removed.
+
+                        break;
+                    case 'EVTS_CACHE':
+                        const eventGrpX2 = _.cloneDeep(eventGrp);
+
+                        eventGrpX2.events = _.filter(eventGrpX2.events, (ev) =>
+                            !_.includes(['EVT_CACHE_OBJECT_SWAPPED', 'EVT_CACHE_OBJECT_UNSWAPPED'], ev));
+
+                        acc.push(eventGrpX2);
+
+                        break;
+                    default:
+                        acc.push(eventGrp);
+                }
+
+                return acc;
+            }, []);
+        }
+
+        return eventGrps;
+    }
+
+    // Generate events group.
+    static clusterEncryption(encryption, available, cfg = this.igniteConfigurationBean(cluster)) {
+        if (!available('2.7.0'))
+            return cfg;
+
+        let bean;
+
+        switch (_.get(encryption, 'kind')) {
+            case 'Keystore':
+                bean = new Bean('org.apache.ignite.spi.encryption.keystore.KeystoreEncryptionSpi', 'encryptionSpi',
+                    encryption.Keystore, clusterDflts.encryptionSpi.Keystore)
+                    .stringProperty('keyStorePath');
+
+                if (nonEmpty(bean.valueOf('keyStorePath')))
+                    bean.propertyChar('keyStorePassword', 'encryption.key.storage.password', 'YOUR_ENCRYPTION_KEY_STORAGE_PASSWORD');
+
+
+                bean.intProperty('keySize')
+                    .stringProperty('masterKeyName');
+
+                break;
+
+            case 'Custom':
+                const clsName = _.get(encryption, 'Custom.className');
+
+                if (clsName)
+                    bean = new EmptyBean(clsName);
+
+                break;
+
+            default:
+                // No-op.
+        }
+
+        if (bean)
+            cfg.beanProperty('encryptionSpi', bean);
+
+        return cfg;
+    }
+
+    // Generate events group.
+    static clusterEvents(cluster, available, cfg = this.igniteConfigurationBean(cluster)) {
+        const eventStorage = cluster.eventStorage;
+
+        let eventStorageBean = null;
+
+        switch (_.get(eventStorage, 'kind')) {
+            case 'Memory':
+                eventStorageBean = new Bean('org.apache.ignite.spi.eventstorage.memory.MemoryEventStorageSpi', 'eventStorage', eventStorage.Memory, clusterDflts.eventStorage.Memory);
+
+                eventStorageBean.longProperty('expireAgeMs')
+                    .longProperty('expireCount')
+                    .emptyBeanProperty('filter');
+
+                break;
+
+            case 'Custom':
+                const className = _.get(eventStorage, 'Custom.className');
+
+                if (className)
+                    eventStorageBean = new EmptyBean(className);
+
+                break;
+
+            default:
+                // No-op.
+        }
+
+        if (eventStorageBean) {
+            if (!eventStorageBean.isEmpty() || !available(['1.0.0', '2.0.0']))
+                cfg.beanProperty('eventStorageSpi', eventStorageBean);
+
+            if (nonEmpty(cluster.includeEventTypes)) {
+                const eventGrps = _.filter(this.eventGrps, ({value}) => _.includes(cluster.includeEventTypes, value));
+
+                cfg.eventTypes('evts', 'includeEventTypes', this.filterEvents(eventGrps, available));
+            }
+        }
+
+        cfg.mapProperty('localEventListeners', _.map(cluster.localEventListeners,
+            (lnr) => ({className: new EmptyBean(lnr.className), eventTypes: _.map(lnr.eventTypes, (evt) => {
+                const grp = _.find(this.eventGrps, ((grp) => grp.events.indexOf(evt) >= 0));
+
+                return {class: grp.class, label: evt};
+            })})), 'localEventListeners');
+
+        return cfg;
+    }
+
+    // Generate failover group.
+    static clusterFailover(cluster, available, cfg = this.igniteConfigurationBean(cluster)) {
+        const spis = [];
+
+        // Since ignite 2.0
+        if (available('2.0.0')) {
+            cfg.longProperty('failureDetectionTimeout')
+                .longProperty('clientFailureDetectionTimeout');
+
+            if (available('2.7.0'))
+                cfg.longProperty('systemWorkerBlockedTimeout');
+        }
+
+        _.forEach(cluster.failoverSpi, (spi) => {
+            let failoverSpi;
+
+            switch (_.get(spi, 'kind')) {
+                case 'JobStealing':
+                    failoverSpi = new Bean('org.apache.ignite.spi.failover.jobstealing.JobStealingFailoverSpi',
+                        'failoverSpi', spi.JobStealing, clusterDflts.failoverSpi.JobStealing);
+
+                    failoverSpi.intProperty('maximumFailoverAttempts');
+
+                    break;
+                case 'Never':
+                    failoverSpi = new Bean('org.apache.ignite.spi.failover.never.NeverFailoverSpi',
+                        'failoverSpi', spi.Never);
+
+                    break;
+                case 'Always':
+                    failoverSpi = new Bean('org.apache.ignite.spi.failover.always.AlwaysFailoverSpi',
+                        'failoverSpi', spi.Always, clusterDflts.failoverSpi.Always);
+
+                    failoverSpi.intProperty('maximumFailoverAttempts');
+
+                    break;
+                case 'Custom':
+                    const className = _.get(spi, 'Custom.class');
+
+                    if (className)
+                        failoverSpi = new EmptyBean(className);
+
+                    break;
+                default:
+                    // No-op.
+            }
+
+            if (failoverSpi)
+                spis.push(failoverSpi);
+        });
+
+        if (spis.length)
+            cfg.arrayProperty('failoverSpi', 'failoverSpi', spis, 'org.apache.ignite.spi.failover.FailoverSpi');
+
+        if (available('2.5.0')) {
+            const handler = cluster.failureHandler;
+            const kind = _.get(handler, 'kind');
+
+            let bean;
+
+            switch (kind) {
+                case 'RestartProcess':
+                    bean = new Bean('org.apache.ignite.failure.RestartProcessFailureHandler', 'failureHandler', handler);
+
+                    break;
+
+                case 'StopNodeOnHalt':
+                    const failover = handler.StopNodeOnHalt;
+
+                    bean = new Bean('org.apache.ignite.failure.StopNodeOrHaltFailureHandler', 'failureHandler', handler.StopNodeOnHalt);
+
+                    if (failover || failover.tryStop || failover.timeout) {
+                        failover.tryStop = failover.tryStop || false;
+                        failover.timeout = failover.timeout || 0;
+
+                        bean.boolConstructorArgument('tryStop')
+                            .longConstructorArgument('timeout');
+                    }
+
+                    break;
+
+                case 'StopNode':
+                    bean = new Bean('org.apache.ignite.failure.StopNodeFailureHandler', 'failureHandler', handler);
+
+                    break;
+
+                case 'Noop':
+                    bean = new Bean('org.apache.ignite.failure.NoOpFailureHandler', 'failureHandler', handler);
+
+                    break;
+
+                case 'Custom':
+                    const clsName = _.get(handler, 'Custom.className');
+
+                    if (clsName)
+                        bean = new Bean(clsName, 'failureHandler', handler);
+
+                    break;
+
+                default:
+                    // No-op.
+            }
+
+            if (bean) {
+                if (['RestartProcess', 'StopNodeOnHalt', 'StopNode'].indexOf(kind) >= 0) {
+                    bean.collectionProperty('ignoredFailureTypes', 'ignoredFailureTypes', handler.ignoredFailureTypes,
+                        'org.apache.ignite.failure.FailureType', 'java.util.HashSet');
+                }
+
+                cfg.beanProperty('failureHandler', bean);
+            }
+        }
+
+        return cfg;
+    }
+
+    // Generate failover group.
+    static clusterHadoop(hadoop, cfg = this.igniteConfigurationBean()) {
+        const hadoopBean = new Bean('org.apache.ignite.configuration.HadoopConfiguration', 'hadoop', hadoop, clusterDflts.hadoopConfiguration);
+
+        let plannerBean;
+
+        switch (_.get(hadoop, 'mapReducePlanner.kind')) {
+            case 'Weighted':
+                plannerBean = new Bean('org.apache.ignite.hadoop.mapreduce.IgniteHadoopWeightedMapReducePlanner', 'planner',
+                    _.get(hadoop, 'mapReducePlanner.Weighted'), clusterDflts.hadoopConfiguration.mapReducePlanner.Weighted);
+
+                plannerBean.intProperty('localMapperWeight')
+                    .intProperty('remoteMapperWeight')
+                    .intProperty('localReducerWeight')
+                    .intProperty('remoteReducerWeight')
+                    .intProperty('preferLocalReducerThresholdWeight');
+
+                break;
+
+            case 'Custom':
+                const clsName = _.get(hadoop, 'mapReducePlanner.Custom.className');
+
+                if (clsName)
+                    plannerBean = new EmptyBean(clsName);
+
+                break;
+
+            default:
+                // No-op.
+        }
+
+        if (plannerBean)
+            hadoopBean.beanProperty('mapReducePlanner', plannerBean);
+
+        hadoopBean.longProperty('finishedJobInfoTtl')
+            .intProperty('maxParallelTasks')
+            .intProperty('maxTaskQueueSize')
+            .arrayProperty('nativeLibraryNames', 'nativeLibraryNames', _.get(hadoop, 'nativeLibraryNames'));
+
+        if (!hadoopBean.isEmpty())
+            cfg.beanProperty('hadoopConfiguration', hadoopBean);
+
+        return cfg;
+    }
+
+    // Generate load balancing configuration group.
+    static clusterLoadBalancing(cluster, cfg = this.igniteConfigurationBean(cluster)) {
+        const spis = [];
+
+        _.forEach(cluster.loadBalancingSpi, (spi) => {
+            let loadBalancingSpi;
+
+            switch (_.get(spi, 'kind')) {
+                case 'RoundRobin':
+                    loadBalancingSpi = new Bean('org.apache.ignite.spi.loadbalancing.roundrobin.RoundRobinLoadBalancingSpi', 'loadBalancingSpiRR', spi.RoundRobin, clusterDflts.loadBalancingSpi.RoundRobin);
+
+                    loadBalancingSpi.boolProperty('perTask');
+
+                    break;
+
+                case 'Adaptive':
+                    loadBalancingSpi = new Bean('org.apache.ignite.spi.loadbalancing.adaptive.AdaptiveLoadBalancingSpi', 'loadBalancingSpiAdaptive', spi.Adaptive);
+
+                    let probeBean;
+
+                    switch (_.get(spi, 'Adaptive.loadProbe.kind')) {
+                        case 'Job':
+                            probeBean = new Bean('org.apache.ignite.spi.loadbalancing.adaptive.AdaptiveJobCountLoadProbe', 'jobProbe', spi.Adaptive.loadProbe.Job, clusterDflts.loadBalancingSpi.Adaptive.loadProbe.Job);
+
+                            probeBean.boolProperty('useAverage');
+
+                            break;
+
+                        case 'CPU':
+                            probeBean = new Bean('org.apache.ignite.spi.loadbalancing.adaptive.AdaptiveCpuLoadProbe', 'cpuProbe', spi.Adaptive.loadProbe.CPU, clusterDflts.loadBalancingSpi.Adaptive.loadProbe.CPU);
+
+                            probeBean.boolProperty('useAverage')
+                                .boolProperty('useProcessors')
+                                .intProperty('processorCoefficient');
+
+                            break;
+
+                        case 'ProcessingTime':
+                            probeBean = new Bean('org.apache.ignite.spi.loadbalancing.adaptive.AdaptiveProcessingTimeLoadProbe', 'timeProbe', spi.Adaptive.loadProbe.ProcessingTime, clusterDflts.loadBalancingSpi.Adaptive.loadProbe.ProcessingTime);
+
+                            probeBean.boolProperty('useAverage');
+
+                            break;
+
+                        case 'Custom':
+                            const className = _.get(spi, 'Adaptive.loadProbe.Custom.className');
+
+                            if (className)
+                                probeBean = new Bean(className, 'probe', spi.Adaptive.loadProbe.Job.Custom);
+
+                            break;
+
+                        default:
+                            // No-op.
+                    }
+
+                    if (probeBean)
+                        loadBalancingSpi.beanProperty('loadProbe', probeBean);
+
+                    break;
+
+                case 'WeightedRandom':
+                    loadBalancingSpi = new Bean('org.apache.ignite.spi.loadbalancing.weightedrandom.WeightedRandomLoadBalancingSpi', 'loadBalancingSpiRandom', spi.WeightedRandom, clusterDflts.loadBalancingSpi.WeightedRandom);
+
+                    loadBalancingSpi.intProperty('nodeWeight')
+                        .boolProperty('useWeights');
+
+                    break;
+
+                case 'Custom':
+                    const cusClassName = _.get(spi, 'Custom.className');
+
+                    if (cusClassName)
+                        loadBalancingSpi = new Bean(cusClassName, 'loadBalancingSpiCustom', spi.Custom);
+
+                    break;
+
+                default:
+                    // No-op.
+            }
+
+            if (loadBalancingSpi)
+                spis.push(loadBalancingSpi);
+        });
+
+        if (spis.length)
+            cfg.varArgProperty('loadBalancingSpi', 'loadBalancingSpi', spis, 'org.apache.ignite.spi.loadbalancing.LoadBalancingSpi');
+
+        return cfg;
+    }
+
+    // Generate logger group.
+    static clusterLogger(logger, cfg = this.igniteConfigurationBean()) {
+        let loggerBean;
+
+        switch (_.get(logger, 'kind')) {
+            case 'Log4j':
+                if (logger.Log4j && (logger.Log4j.mode === 'Default' || logger.Log4j.mode === 'Path' && nonEmpty(logger.Log4j.path))) {
+                    loggerBean = new Bean('org.apache.ignite.logger.log4j.Log4JLogger',
+                        'logger', logger.Log4j, clusterDflts.logger.Log4j);
+
+                    if (loggerBean.valueOf('mode') === 'Path')
+                        loggerBean.pathConstructorArgument('path');
+
+                    loggerBean.enumProperty('level');
+                }
+
+                break;
+            case 'Log4j2':
+                if (logger.Log4j2 && nonEmpty(logger.Log4j2.path)) {
+                    loggerBean = new Bean('org.apache.ignite.logger.log4j2.Log4J2Logger',
+                        'logger', logger.Log4j2, clusterDflts.logger.Log4j2);
+
+                    loggerBean.pathConstructorArgument('path')
+                        .enumProperty('level');
+                }
+
+                break;
+            case 'Null':
+                loggerBean = new EmptyBean('org.apache.ignite.logger.NullLogger');
+
+                break;
+            case 'Java':
+                loggerBean = new EmptyBean('org.apache.ignite.logger.java.JavaLogger');
+
+                break;
+            case 'JCL':
+                loggerBean = new EmptyBean('org.apache.ignite.logger.jcl.JclLogger');
+
+                break;
+            case 'SLF4J':
+                loggerBean = new EmptyBean('org.apache.ignite.logger.slf4j.Slf4jLogger');
+
+                break;
+            case 'Custom':
+                if (logger.Custom && nonEmpty(logger.Custom.class))
+                    loggerBean = new EmptyBean(logger.Custom.class);
+
+                break;
+            default:
+                return cfg;
+        }
+
+        if (loggerBean)
+            cfg.beanProperty('gridLogger', loggerBean);
+
+        return cfg;
+    }
+
+    // Generate memory configuration.
+    static clusterMemory(memoryConfiguration, available, cfg = this.igniteConfigurationBean()) {
+        const memoryBean = new Bean('org.apache.ignite.configuration.MemoryConfiguration', 'memoryConfiguration', memoryConfiguration, clusterDflts.memoryConfiguration);
+
+        memoryBean.intProperty('pageSize')
+            .intProperty('concurrencyLevel')
+            .longProperty('systemCacheInitialSize')
+            .longProperty('systemCacheMaxSize')
+            .stringProperty('defaultMemoryPolicyName');
+
+        if (memoryBean.valueOf('defaultMemoryPolicyName') === 'default')
+            memoryBean.longProperty('defaultMemoryPolicySize');
+
+        const policies = [];
+
+        _.forEach(_.get(memoryConfiguration, 'memoryPolicies'), (plc) => {
+            const plcBean = new Bean('org.apache.ignite.configuration.MemoryPolicyConfiguration', 'policy', plc, clusterDflts.memoryConfiguration.memoryPolicies);
+
+            plcBean.stringProperty('name')
+                .longProperty('initialSize')
+                .longProperty('maxSize')
+                .stringProperty('swapFilePath')
+                .enumProperty('pageEvictionMode')
+                .doubleProperty('evictionThreshold')
+                .intProperty('emptyPagesPoolSize')
+                .boolProperty('metricsEnabled');
+
+            if (available('2.1.0')) {
+                plcBean.intProperty('subIntervals')
+                    .longProperty('rateTimeInterval');
+            }
+
+            if (plcBean.isEmpty())
+                return;
+
+            policies.push(plcBean);
+        });
+
+        if (!_.isEmpty(policies))
+            memoryBean.varArgProperty('memoryPolicies', 'memoryPolicies', policies, 'org.apache.ignite.configuration.MemoryPolicyConfiguration');
+
+        if (memoryBean.isEmpty())
+            return cfg;
+
+        cfg.beanProperty('memoryConfiguration', memoryBean);
+
+        return cfg;
+    }
+
+    static dataRegionConfiguration(dataRegionCfg) {
+        const plcBean = new Bean('org.apache.ignite.configuration.DataRegionConfiguration', 'dataRegionCfg', dataRegionCfg, clusterDflts.dataStorageConfiguration.dataRegionConfigurations);
+
+        plcBean.stringProperty('name')
+            .longProperty('initialSize')
+            .longProperty('maxSize')
+            .stringProperty('swapPath')
+            .enumProperty('pageEvictionMode')
+            .doubleProperty('evictionThreshold')
+            .intProperty('emptyPagesPoolSize')
+            .intProperty('metricsSubIntervalCount')
+            .longProperty('metricsRateTimeInterval')
+            .longProperty('checkpointPageBufferSize')
+            .boolProperty('metricsEnabled');
+
+        if (!plcBean.valueOf('swapPath'))
+            plcBean.boolProperty('persistenceEnabled');
+
+        return plcBean;
+    }
+
+    // Generate data storage configuration.
+    static clusterDataStorageConfiguration(cluster, available, cfg = this.igniteConfigurationBean(cluster)) {
+        if (!available('2.3.0'))
+            return cfg;
+
+        const available2_4 = available('2.4.0');
+
+        const available2_7 = available('2.7.0');
+
+        const dataStorageCfg = cluster.dataStorageConfiguration;
+
+        const storageBean = new Bean('org.apache.ignite.configuration.DataStorageConfiguration', 'dataStorageCfg', dataStorageCfg, clusterDflts.dataStorageConfiguration);
+
+        storageBean.intProperty('pageSize')
+            .intProperty('concurrencyLevel')
+            .longProperty('systemRegionInitialSize')
+            .longProperty('systemRegionMaxSize');
+
+        const dfltDataRegionCfg = this.dataRegionConfiguration(_.get(dataStorageCfg, 'defaultDataRegionConfiguration'));
+
+        if (!dfltDataRegionCfg.isEmpty())
+            storageBean.beanProperty('defaultDataRegionConfiguration', dfltDataRegionCfg);
+
+        const dataRegionCfgs = [];
+
+        _.forEach(_.get(dataStorageCfg, 'dataRegionConfigurations'), (dataRegionCfg) => {
+            const plcBean = this.dataRegionConfiguration(dataRegionCfg);
+
+            if (plcBean.isEmpty())
+                return;
+
+            dataRegionCfgs.push(plcBean);
+        });
+
+        if (!_.isEmpty(dataRegionCfgs))
+            storageBean.varArgProperty('dataRegionConfigurations', 'dataRegionConfigurations', dataRegionCfgs, 'org.apache.ignite.configuration.DataRegionConfiguration');
+
+        storageBean.stringProperty('storagePath')
+            .longProperty('checkpointFrequency');
+
+        if (available2_7) {
+            storageBean
+                .longProperty('checkpointReadLockTimeout');
+        }
+
+        storageBean.intProperty('checkpointThreads')
+            .enumProperty('checkpointWriteOrder')
+            .enumProperty('walMode')
+            .stringProperty('walPath')
+            .stringProperty('walArchivePath');
+
+        if (available2_7) {
+            storageBean.longProperty('maxWalArchiveSize')
+                .intProperty('walCompactionLevel');
+        }
+
+        storageBean.longProperty('walAutoArchiveAfterInactivity')
+            .intProperty('walSegments')
+            .intProperty('walSegmentSize')
+            .intProperty('walHistorySize');
+
+        if (available2_4)
+            storageBean.intProperty('walBufferSize');
+
+        storageBean.longProperty('walFlushFrequency')
+            .longProperty('walFsyncDelayNanos')
+            .intProperty('walRecordIteratorBufferSize')
+            .longProperty('lockWaitTime')
+            .intProperty('walThreadLocalBufferSize')
+            .intProperty('metricsSubIntervalCount')
+            .longProperty('metricsRateTimeInterval')
+            .boolProperty('metricsEnabled')
+            .boolProperty('alwaysWriteFullPages')
+            .boolProperty('writeThrottlingEnabled');
+
+        if (available2_4)
+            storageBean.boolProperty('walCompactionEnabled');
+
+        const fileIOFactory = _.get(dataStorageCfg, 'fileIOFactory');
+
+        let factoryBean;
+
+        if (fileIOFactory === 'RANDOM')
+            factoryBean = new Bean('org.apache.ignite.internal.processors.cache.persistence.file.RandomAccessFileIOFactory', 'rndFileIoFactory', {});
+        else if (fileIOFactory === 'ASYNC')
+            factoryBean = new Bean('org.apache.ignite.internal.processors.cache.persistence.file.AsyncFileIOFactory', 'asyncFileIoFactory', {});
+
+        if (factoryBean)
+            storageBean.beanProperty('fileIOFactory', factoryBean);
+
+        if (_.get(dataStorageCfg, 'defaultDataRegionConfiguration.persistenceEnabled')
+            || _.find(_.get(dataStorageCfg, 'dataRegionConfigurations'), (storeCfg) => storeCfg.persistenceEnabled))
+            cfg.boolProperty('authenticationEnabled');
+
+        if (storageBean.nonEmpty())
+            cfg.beanProperty('dataStorageConfiguration', storageBean);
+
+        return cfg;
+    }
+
+    // Generate miscellaneous configuration.
+    static clusterMisc(cluster, available, cfg = this.igniteConfigurationBean(cluster)) {
+        const available2_0 = available('2.0.0');
+
+        cfg.pathProperty('workDirectory')
+            .pathProperty('igniteHome')
+            .varArgProperty('lifecycleBeans', 'lifecycleBeans', _.map(cluster.lifecycleBeans, (bean) => new EmptyBean(bean)), 'org.apache.ignite.lifecycle.LifecycleBean')
+            .emptyBeanProperty('addressResolver')
+            .emptyBeanProperty('mBeanServer')
+            .varArgProperty('includeProperties', 'includeProperties', cluster.includeProperties);
+
+        if (cluster.cacheStoreSessionListenerFactories) {
+            const factories = _.map(cluster.cacheStoreSessionListenerFactories, (factory) => new EmptyBean(factory));
+
+            cfg.varArgProperty('cacheStoreSessionListenerFactories', 'cacheStoreSessionListenerFactories', factories, 'javax.cache.configuration.Factory');
+        }
+
+        if (available2_0) {
+            cfg
+                .stringProperty('consistentId')
+                .emptyBeanProperty('warmupClosure')
+                .boolProperty('activeOnStart')
+                .boolProperty('cacheSanityCheckEnabled');
+        }
+
+        if (available('2.7.0'))
+            cfg.varArgProperty('sqlSchemas', 'sqlSchemas', cluster.sqlSchemas);
+
+        if (available('2.8.0'))
+            cfg.intProperty('sqlQueryHistorySize');
+
+        if (available('2.4.0'))
+            cfg.boolProperty('autoActivationEnabled');
+
+        if (available(['1.0.0', '2.1.0']))
+            cfg.boolProperty('lateAffinityAssignment');
+
+        return cfg;
+    }
+
+    // Generate MVCC configuration.
+    static clusterMvcc(cluster, available, cfg = this.igniteConfigurationBean(cluster)) {
+        if (available('2.7.0')) {
+            cfg.intProperty('mvccVacuumThreadCount')
+                .intProperty('mvccVacuumFrequency');
+        }
+
+        return cfg;
+    }
+
+    // Generate IGFSs configs.
+    static clusterIgfss(igfss, available, cfg = this.igniteConfigurationBean()) {
+        const igfsCfgs = _.map(igfss, (igfs) => {
+            const igfsCfg = this.igfsGeneral(igfs, available);
+
+            this.igfsIPC(igfs, igfsCfg);
+            this.igfsFragmentizer(igfs, igfsCfg);
+
+            // Removed in ignite 2.0
+            if (available(['1.0.0', '2.0.0']))
+                this.igfsDualMode(igfs, igfsCfg);
+
+            this.igfsSecondFS(igfs, igfsCfg);
+            this.igfsMisc(igfs, available, igfsCfg);
+
+            return igfsCfg;
+        });
+
+        cfg.varArgProperty('igfsCfgs', 'fileSystemConfiguration', igfsCfgs, 'org.apache.ignite.configuration.FileSystemConfiguration');
+
+        return cfg;
+    }
+
+    // Generate marshaller group.
+    static clusterMarshaller(cluster, available, cfg = this.igniteConfigurationBean(cluster)) {
+        if (available(['1.0.0', '2.1.0'])) {
+            const kind = _.get(cluster.marshaller, 'kind');
+            const settings = _.get(cluster.marshaller, kind);
+
+            let bean;
+
+            switch (kind) {
+                case 'OptimizedMarshaller':
+                    if (available(['1.0.0', '2.0.0'])) {
+                        bean = new Bean('org.apache.ignite.marshaller.optimized.OptimizedMarshaller', 'marshaller', settings)
+                            .intProperty('poolSize')
+                            .intProperty('requireSerializable');
+                    }
+
+                    break;
+
+                case 'JdkMarshaller':
+                    bean = new Bean('org.apache.ignite.marshaller.jdk.JdkMarshaller', 'marshaller', settings);
+
+                    break;
+
+                default:
+                // No-op.
+            }
+
+            if (bean)
+                cfg.beanProperty('marshaller', bean);
+        }
+
+        cfg.intProperty('marshalLocalJobs');
+
+        // Removed in ignite 2.0
+        if (available(['1.0.0', '2.0.0'])) {
+            cfg.intProperty('marshallerCacheKeepAliveTime')
+                .intProperty('marshallerCacheThreadPoolSize', 'marshallerCachePoolSize');
+        }
+
+        return cfg;
+    }
+
+    // Generate metrics group.
+    static clusterMetrics(cluster, available, cfg = this.igniteConfigurationBean(cluster)) {
+        cfg.longProperty('metricsExpireTime')
+            .intProperty('metricsHistorySize')
+            .longProperty('metricsLogFrequency');
+
+        // Since ignite 2.0
+        if (available('2.0.0'))
+            cfg.longProperty('metricsUpdateFrequency');
+
+        return cfg;
+    }
+
+    // Generate ODBC group.
+    static clusterODBC(odbc, available, cfg = this.igniteConfigurationBean()) {
+        //  Deprecated in ignite 2.1
+        if (available('2.1.0') || _.get(odbc, 'odbcEnabled') !== true)
+            return cfg;
+
+        const bean = new Bean('org.apache.ignite.configuration.OdbcConfiguration', 'odbcConfiguration',
+            odbc, clusterDflts.odbcConfiguration);
+
+        bean.stringProperty('endpointAddress')
+            .intProperty('socketSendBufferSize')
+            .intProperty('socketReceiveBufferSize')
+            .intProperty('maxOpenCursors')
+            .intProperty('threadPoolSize');
+
+        cfg.beanProperty('odbcConfiguration', bean);
+
+        return cfg;
+    }
+
+    // Generate cluster query group.
+    static clusterQuery(cluster, available, cfg = this.igniteConfigurationBean(cluster)) {
+        if (!available('2.1.0'))
+            return cfg;
+
+        cfg.longProperty('longQueryWarningTimeout');
+
+        if (_.get(cluster, 'sqlConnectorConfiguration.enabled') !== true)
+            return cfg;
+
+        const bean = new Bean('org.apache.ignite.configuration.SqlConnectorConfiguration', 'sqlConnCfg',
+            cluster.sqlConnectorConfiguration, clusterDflts.sqlConnectorConfiguration);
+
+        bean.stringProperty('host')
+            .intProperty('port')
+            .intProperty('portRange')
+            .intProperty('socketSendBufferSize')
+            .intProperty('socketReceiveBufferSize')
+            .intProperty('maxOpenCursorsPerConnection')
+            .intProperty('threadPoolSize')
+            .boolProperty('tcpNoDelay');
+
+        cfg.beanProperty('sqlConnectorConfiguration', bean);
+
+        return cfg;
+    }
+
+    // Generate cluster query group.
+    static clusterPersistence(persistence, available, cfg = this.igniteConfigurationBean()) {
+        if (!available(['2.1.0', '2.3.0']) || _.get(persistence, 'enabled') !== true)
+            return cfg;
+
+        const bean = new Bean('org.apache.ignite.configuration.PersistentStoreConfiguration', 'PersistenceCfg',
+            persistence, clusterDflts.persistenceStoreConfiguration);
+
+        bean.stringProperty('persistentStorePath')
+            .boolProperty('metricsEnabled')
+            .boolProperty('alwaysWriteFullPages')
+            .longProperty('checkpointingFrequency')
+            .longProperty('checkpointingPageBufferSize')
+            .intProperty('checkpointingThreads')
+            .enumProperty('walMode')
+            .stringProperty('walStorePath')
+            .stringProperty('walArchivePath')
+            .intProperty('walSegments')
+            .intProperty('walSegmentSize')
+            .intProperty('walHistorySize')
+            .longProperty('walFlushFrequency')
+            .longProperty('walFsyncDelayNanos')
+            .intProperty('walRecordIteratorBufferSize')
+            .longProperty('lockWaitTime')
+            .longProperty('rateTimeInterval')
+            .intProperty('tlbSize')
+            .intProperty('subIntervals')
+            .longProperty('walAutoArchiveAfterInactivity');
+
+        cfg.beanProperty('persistentStoreConfiguration', bean);
+
+        return cfg;
+    }
+
+    // Java code generator for cluster's service configurations.
+    static clusterServiceConfiguration(srvs, caches, cfg = this.igniteConfigurationBean()) {
+        const srvBeans = [];
+
+        _.forEach(srvs, (srv) => {
+            const bean = new Bean('org.apache.ignite.services.ServiceConfiguration', 'service', srv, clusterDflts.serviceConfigurations);
+
+            bean.stringProperty('name')
+                .emptyBeanProperty('service')
+                .intProperty('maxPerNodeCount')
+                .intProperty('totalCount')
+                .stringProperty('cache', 'cacheName', (_id) => _id ? _.get(_.find(caches, {_id}), 'name', null) : null)
+                .stringProperty('affinityKey');
+
+            srvBeans.push(bean);
+        });
+
+        if (!_.isEmpty(srvBeans))
+            cfg.arrayProperty('services', 'serviceConfiguration', srvBeans, 'org.apache.ignite.services.ServiceConfiguration');
+
+        return cfg;
+    }
+
+    // Java code generator for cluster's SSL configuration.
+    static clusterSsl(cluster, available, cfg = this.igniteConfigurationBean(cluster)) {
+        if (cluster.sslEnabled && nonNil(cluster.sslContextFactory)) {
+            const bean = new Bean('org.apache.ignite.ssl.SslContextFactory', 'sslCtxFactory',
+                cluster.sslContextFactory);
+
+            bean.intProperty('keyAlgorithm')
+                .pathProperty('keyStoreFilePath');
+
+            if (nonEmpty(bean.valueOf('keyStoreFilePath')))
+                bean.propertyChar('keyStorePassword', 'ssl.key.storage.password', 'YOUR_SSL_KEY_STORAGE_PASSWORD');
+
+            bean.intProperty('keyStoreType')
+                .intProperty('protocol');
+
+            if (nonEmpty(cluster.sslContextFactory.trustManagers)) {
+                bean.arrayProperty('trustManagers', 'trustManagers',
+                    _.map(cluster.sslContextFactory.trustManagers, (clsName) => new EmptyBean(clsName)),
+                    'javax.net.ssl.TrustManager');
+            }
+            else {
+                bean.pathProperty('trustStoreFilePath');
+
+                if (nonEmpty(bean.valueOf('trustStoreFilePath')))
+                    bean.propertyChar('trustStorePassword', 'ssl.trust.storage.password', 'YOUR_SSL_TRUST_STORAGE_PASSWORD');
+
+                bean.intProperty('trustStoreType');
+            }
+
+            if (available('2.7.0')) {
+                bean.varArgProperty('cipherSuites', 'cipherSuites', cluster.sslContextFactory.cipherSuites)
+                    .varArgProperty('protocols', 'protocols', cluster.sslContextFactory.protocols);
+            }
+
+            cfg.beanProperty('sslContextFactory', bean);
+        }
+
+        return cfg;
+    }
+
+    // Generate swap group.
+    static clusterSwap(cluster, cfg = this.igniteConfigurationBean(cluster)) {
+        if (_.get(cluster.swapSpaceSpi, 'kind') === 'FileSwapSpaceSpi') {
+            const bean = new Bean('org.apache.ignite.spi.swapspace.file.FileSwapSpaceSpi', 'swapSpaceSpi',
+                cluster.swapSpaceSpi.FileSwapSpaceSpi);
+
+            bean.pathProperty('baseDirectory')
+                .intProperty('readStripesNumber')
+                .floatProperty('maximumSparsity')
+                .intProperty('maxWriteQueueSize')
+                .intProperty('writeBufferSize');
+
+            cfg.beanProperty('swapSpaceSpi', bean);
+        }
+
+        return cfg;
+    }
+
+    // Generate time group.
+    static clusterTime(cluster, available, cfg = this.igniteConfigurationBean(cluster)) {
+        if (available(['1.0.0', '2.0.0'])) {
+            cfg.intProperty('clockSyncSamples')
+                .intProperty('clockSyncFrequency');
+        }
+
+        cfg.intProperty('timeServerPortBase')
+            .intProperty('timeServerPortRange');
+
+        return cfg;
+    }
+
+    // Generate thread pools group.
+    static clusterPools(cluster, available, cfg = this.igniteConfigurationBean(cluster)) {
+        cfg.intProperty('publicThreadPoolSize')
+            .intProperty('systemThreadPoolSize')
+            .intProperty('serviceThreadPoolSize')
+            .intProperty('managementThreadPoolSize')
+            .intProperty('igfsThreadPoolSize')
+            .intProperty('rebalanceThreadPoolSize')
+            .intProperty('utilityCacheThreadPoolSize', 'utilityCachePoolSize')
+            .longProperty('utilityCacheKeepAliveTime')
+            .intProperty('asyncCallbackPoolSize')
+            .intProperty('stripedPoolSize');
+
+        // Since ignite 2.0
+        if (available('2.0.0')) {
+            cfg.intProperty('dataStreamerThreadPoolSize')
+                .intProperty('queryThreadPoolSize');
+
+            const executors = [];
+
+            _.forEach(cluster.executorConfiguration, (exec) => {
+                const execBean = new Bean('org.apache.ignite.configuration.ExecutorConfiguration', 'executor', exec);
+
+                execBean.stringProperty('name')
+                    .intProperty('size');
+
+                if (!execBean.isEmpty())
+                    executors.push(execBean);
+            });
+
+            if (!_.isEmpty(executors))
+                cfg.arrayProperty('executors', 'executorConfiguration', executors, 'org.apache.ignite.configuration.ExecutorConfiguration');
+        }
+
+        return cfg;
+    }
+
+    // Generate transactions group.
+    static clusterTransactions(transactionConfiguration, available, cfg = this.igniteConfigurationBean()) {
+        const bean = new Bean('org.apache.ignite.configuration.TransactionConfiguration', 'transactionConfiguration',
+            transactionConfiguration, clusterDflts.transactionConfiguration);
+
+        bean.enumProperty('defaultTxConcurrency')
+            .enumProperty('defaultTxIsolation')
+            .longProperty('defaultTxTimeout')
+            .intProperty('pessimisticTxLogLinger')
+            .intProperty('pessimisticTxLogSize')
+            .boolProperty('txSerializableEnabled')
+            .emptyBeanProperty('txManagerFactory');
+
+        if (available('2.5.0'))
+            bean.longProperty('txTimeoutOnPartitionMapExchange');
+
+        if (available('2.8.0'))
+            bean.longProperty('deadlockTimeout');
+
+        bean.boolProperty('useJtaSynchronization');
+
+        if (bean.nonEmpty())
+            cfg.beanProperty('transactionConfiguration', bean);
+
+        return cfg;
+    }
+
+    // Generate user attributes group.
+    static clusterUserAttributes(cluster, cfg = this.igniteConfigurationBean(cluster)) {
+        cfg.mapProperty('attrs', 'attributes', 'userAttributes');
+
+        return cfg;
+    }
+
+    // Generate domain model for general group.
+    static domainModelGeneral(domain, cfg = this.domainConfigurationBean(domain)) {
+        switch (cfg.valueOf('queryMetadata')) {
+            case 'Annotations':
+                if (nonNil(domain.keyType) && nonNil(domain.valueType)) {
+                    cfg.varArgProperty('indexedTypes', 'indexedTypes',
+                        [javaTypes.fullClassName(domain.keyType), javaTypes.fullClassName(domain.valueType)],
+                        'java.lang.Class');
+                }
+
+                break;
+            case 'Configuration':
+                cfg.stringProperty('keyType', 'keyType', (val) => javaTypes.fullClassName(val))
+                    .stringProperty('valueType', 'valueType', (val) => javaTypes.fullClassName(val));
+
+                break;
+            default:
+        }
+
+        return cfg;
+    }
+
+    // Generate domain model for query group.
+    static domainModelQuery(domain, available, cfg = this.domainConfigurationBean(domain)) {
+        if (cfg.valueOf('queryMetadata') === 'Configuration') {
+            const notNull = [];
+            const precisions = [];
+            const scales = [];
+            const defaultValues = [];
+
+            const notNullAvailable = available('2.3.0');
+            const defaultAvailable = available('2.4.0');
+            const precisionAvailable = available('2.7.0');
+
+            const fields = _.filter(_.map(domain.fields,
+                (e) => {
+                    if (notNullAvailable && e.notNull)
+                        notNull.push(e.name);
+
+                    if (defaultAvailable && e.defaultValue) {
+                        let value = e.defaultValue;
+
+                        switch (e.className) {
+                            case 'String':
+                                value = new Bean('java.lang.String', 'value', e).stringConstructorArgument('defaultValue');
+
+                                break;
+
+                            case 'BigDecimal':
+                                value = new Bean('java.math.BigDecimal', 'value', e).stringConstructorArgument('defaultValue');
+
+                                break;
+
+                            case 'byte[]':
+                                value = null;
+
+                                break;
+
+                            default: // No-op.
+                        }
+
+                        defaultValues.push({name: e.name, value});
+                    }
+
+                    if (precisionAvailable && e.precision) {
+                        precisions.push({name: e.name, value: e.precision});
+
+                        if (e.scale)
+                            scales.push({name: e.name, value: e.scale});
+                    }
+
+                    return {name: e.name, className: javaTypes.stringClassName(e.className)};
+                }), (field) => {
+                return field.name !== domain.keyFieldName && field.name !== domain.valueFieldName;
+            });
+
+            cfg.stringProperty('tableName');
+
+            if (available('2.0.0')) {
+                cfg.stringProperty('keyFieldName')
+                    .stringProperty('valueFieldName');
+
+                const keyFieldName = cfg.valueOf('keyFieldName');
+                const valFieldName = cfg.valueOf('valueFieldName');
+
+                if (keyFieldName)
+                    fields.push({name: keyFieldName, className: javaTypes.stringClassName(domain.keyType)});
+
+                if (valFieldName)
+                    fields.push({name: valFieldName, className: javaTypes.stringClassName(domain.valueType)});
+            }
+
+            cfg.collectionProperty('keyFields', 'keyFields', domain.queryKeyFields, 'java.lang.String', 'java.util.HashSet')
+                .mapProperty('fields', fields, 'fields', true)
+                .mapProperty('aliases', 'aliases');
+
+            if (notNullAvailable && notNull)
+                cfg.collectionProperty('notNullFields', 'notNullFields', notNull, 'java.lang.String', 'java.util.HashSet');
+
+            if (defaultAvailable && defaultValues)
+                cfg.mapProperty('defaultFieldValues', defaultValues, 'defaultFieldValues');
+
+            if (precisionAvailable && precisions) {
+                cfg.mapProperty('fieldsPrecision', precisions, 'fieldsPrecision');
+
+                if (scales)
+                    cfg.mapProperty('fieldsScale', scales, 'fieldsScale');
+            }
+
+            const indexes = _.map(domain.indexes, (index) => {
+                const bean = new Bean('org.apache.ignite.cache.QueryIndex', 'index', index, cacheDflts.indexes)
+                    .stringProperty('name')
+                    .enumProperty('indexType')
+                    .mapProperty('indFlds', 'fields', 'fields', true);
+
+                if (available('2.3.0'))
+                    bean.intProperty('inlineSize');
+
+                return bean;
+            });
+
+            cfg.collectionProperty('indexes', 'indexes', indexes, 'org.apache.ignite.cache.QueryIndex');
+        }
+
+        return cfg;
+    }
+
+    // Generate domain model db fields.
+    static _domainModelDatabaseFields(cfg, propName, domain) {
+        const fields = _.map(domain[propName], (field) => {
+            return new Bean('org.apache.ignite.cache.store.jdbc.JdbcTypeField', 'typeField', field, cacheDflts.typeField)
+                .constantConstructorArgument('databaseFieldType')
+                .stringConstructorArgument('databaseFieldName')
+                .classConstructorArgument('javaFieldType')
+                .stringConstructorArgument('javaFieldName');
+        });
+
+        cfg.varArgProperty(propName, propName, fields, 'org.apache.ignite.cache.store.jdbc.JdbcTypeField');
+
+        return cfg;
+    }
+
+    // Generate domain model for store group.
+    static domainStore(domain, cfg = this.domainJdbcTypeBean(domain)) {
+        cfg.stringProperty('databaseSchema')
+            .stringProperty('databaseTable');
+
+        this._domainModelDatabaseFields(cfg, 'keyFields', domain);
+        this._domainModelDatabaseFields(cfg, 'valueFields', domain);
+
+        return cfg;
+    }
+
+    /**
+     * Generate eviction policy object.
+     * @param {Object} ccfg Parent configuration.
+     * @param {Function} available Function to check feature is supported in Ignite current version.
+     * @param {Boolean} near Near cache flag.
+     * @param {Object} src Source.
+     * @param {Object} dflt Default.
+     * @returns {Object} Parent configuration.
+     * @private
+     */
+    static _evictionPolicy(ccfg, available, near, src, dflt) {
+        let propName;
+        let beanProps;
+
+        if (available('2.4.0')) {
+            switch (_.get(src, 'kind')) {
+                case 'LRU': beanProps = {cls: 'org.apache.ignite.cache.eviction.lru.LruEvictionPolicyFactory', src: src.LRU };
+                    break;
+
+                case 'FIFO': beanProps = {cls: 'org.apache.ignite.cache.eviction.fifo.FifoEvictionPolicyFactory', src: src.FIFO };
+                    break;
+
+                case 'SORTED': beanProps = {cls: 'org.apache.ignite.cache.eviction.sorted.SortedEvictionPolicyFactory', src: src.SORTED };
+                    break;
+
+                default:
+                    return ccfg;
+            }
+
+            propName = (near ? 'nearEviction' : 'eviction') + 'PolicyFactory';
+        }
+        else {
+            switch (_.get(src, 'kind')) {
+                case 'LRU': beanProps = {cls: 'org.apache.ignite.cache.eviction.lru.LruEvictionPolicy', src: src.LRU };
+                    break;
+
+                case 'FIFO': beanProps = {cls: 'org.apache.ignite.cache.eviction.fifo.FifoEvictionPolicy', src: src.FIFO };
+                    break;
+
+                case 'SORTED': beanProps = {cls: 'org.apache.ignite.cache.eviction.sorted.SortedEvictionPolicy', src: src.SORTED };
+                    break;
+
+                default:
+                    return ccfg;
+            }
+
+            propName = (near ? 'nearEviction' : 'eviction') + 'Policy';
+        }
+
+        const bean = new Bean(beanProps.cls, propName, beanProps.src, dflt);
+
+        bean.intProperty('batchSize')
+            .intProperty('maxMemorySize')
+            .intProperty('maxSize');
+
+        ccfg.beanProperty(propName, bean);
+
+        return ccfg;
+    }
+
+    // Generate cache general group.
+    static cacheGeneral(cache, available, ccfg = this.cacheConfigurationBean(cache)) {
+        ccfg.stringProperty('name');
+
+        if (available('2.1.0'))
+            ccfg.stringProperty('groupName');
+
+        ccfg.enumProperty('cacheMode')
+            .enumProperty('atomicityMode');
+
+        if (ccfg.valueOf('cacheMode') === 'PARTITIONED' && ccfg.valueOf('backups')) {
+            ccfg.intProperty('backups')
+                .intProperty('readFromBackup');
+        }
+
+        // Since ignite 2.0
+        if (available('2.0.0'))
+            ccfg.enumProperty('partitionLossPolicy');
+
+        ccfg.intProperty('copyOnRead');
+
+        if (ccfg.valueOf('cacheMode') === 'PARTITIONED' && ccfg.valueOf('atomicityMode') === 'TRANSACTIONAL')
+            ccfg.intProperty('isInvalidate', 'invalidate');
+
+        return ccfg;
+    }
+
+    // Generation of constructor for affinity function.
+    static affinityFunction(cls, func) {
+        const affBean = new Bean(cls, 'affinityFunction', func);
+
+        affBean.boolConstructorArgument('excludeNeighbors')
+            .intProperty('partitions')
+            .emptyBeanProperty('affinityBackupFilter');
+
+        return affBean;
+    }
+
+    // Generate affinity function.
+    static affinity(affinity, cfg) {
+        switch (_.get(affinity, 'kind')) {
+            case 'Rendezvous':
+                cfg.beanProperty('affinity', this.affinityFunction('org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction', affinity.Rendezvous));
+
+                break;
+            case 'Fair':
+                cfg.beanProperty('affinity', this.affinityFunction('org.apache.ignite.cache.affinity.fair.FairAffinityFunction', affinity.Fair));
+
+                break;
+            case 'Custom':
+                cfg.emptyBeanProperty('affinity.Custom.className', 'affinity');
+
+                break;
+            default:
+            // No-op.
+        }
+    }
+
+    // Generate cache memory group.
+    static cacheAffinity(cache, available, ccfg = this.cacheConfigurationBean(cache)) {
+        this.affinity(cache.affinity, ccfg);
+
+        ccfg.emptyBeanProperty('affinityMapper');
+
+        // Since ignite 2.0
+        if (available('2.0.0'))
+            ccfg.emptyBeanProperty('topologyValidator');
+
+        return ccfg;
+    }
+
+    // Generate key configurations of cache.
+    static cacheKeyConfiguration(keyCfgs, available, cfg = this.igniteConfigurationBean()) {
+        if (available('2.1.0')) {
+            const items = _.reduce(keyCfgs, (acc, keyCfg) => {
+                if (keyCfg.typeName && keyCfg.affinityKeyFieldName) {
+                    acc.push(new Bean('org.apache.ignite.cache.CacheKeyConfiguration', null, keyCfg)
+                        .stringConstructorArgument('typeName')
+                        .stringConstructorArgument('affinityKeyFieldName'));
+                }
+
+                return acc;
+            }, []);
+
+            if (_.isEmpty(items))
+                return cfg;
+
+            cfg.arrayProperty('keyConfiguration', 'keyConfiguration', items,
+                'org.apache.ignite.cache.CacheKeyConfiguration');
+        }
+
+        return cfg;
+    }
+
+    // Generate cache memory group.
+    static cacheMemory(cache, available, ccfg = this.cacheConfigurationBean(cache)) {
+        // Since ignite 2.0
+        if (available(['2.0.0', '2.3.0']))
+            ccfg.stringProperty('memoryPolicyName');
+
+        if (available('2.3.0'))
+            ccfg.stringProperty('dataRegionName');
+
+        if (available('2.8.0')) {
+            ccfg.enumProperty('diskPageCompression');
+
+            const compression = ccfg.valueOf('diskPageCompression');
+
+            if (compression === 'ZSTD' || compression === 'LZ4')
+                ccfg.intProperty('diskPageCompressionLevel');
+        }
+
+        // Removed in ignite 2.0
+        if (available(['1.0.0', '2.0.0'])) {
+            ccfg.enumProperty('memoryMode');
+
+            if (ccfg.valueOf('memoryMode') !== 'OFFHEAP_VALUES')
+                ccfg.longProperty('offHeapMaxMemory');
+        }
+
+        // Since ignite 2.0
+        if (available('2.0.0')) {
+            ccfg.boolProperty('onheapCacheEnabled')
+                .emptyBeanProperty('evictionFilter');
+        }
+
+        this._evictionPolicy(ccfg, available, false, cache.evictionPolicy, cacheDflts.evictionPolicy);
+
+        // Removed in ignite 2.0
+        if (available(['1.0.0', '2.0.0'])) {
+            ccfg.intProperty('startSize')
+                .boolProperty('swapEnabled');
+        }
+
+        if (cache.cacheWriterFactory)
+            ccfg.beanProperty('cacheWriterFactory', new EmptyBean(cache.cacheWriterFactory));
+
+        if (cache.cacheLoaderFactory)
+            ccfg.beanProperty('cacheLoaderFactory', new EmptyBean(cache.cacheLoaderFactory));
+
+        if (cache.expiryPolicyFactory)
+            ccfg.beanProperty('expiryPolicyFactory', new EmptyBean(cache.expiryPolicyFactory));
+
+        return ccfg;
+    }
+
+    // Generate cache queries & Indexing group.
+    static cacheQuery(cache, domains, available, ccfg = this.cacheConfigurationBean(cache)) {
+        const indexedTypes = _.reduce(domains, (acc, domain) => {
+            if (domain.queryMetadata === 'Annotations')
+                acc.push(javaTypes.fullClassName(domain.keyType), javaTypes.fullClassName(domain.valueType));
+
+            return acc;
+        }, []);
+
+        ccfg.stringProperty('sqlSchema');
+
+        // Removed in ignite 2.0
+        if (available(['1.0.0', '2.0.0']))
+            ccfg.intProperty('sqlOnheapRowCacheSize');
+
+        ccfg.longProperty('longQueryWarningTimeout')
+            .arrayProperty('indexedTypes', 'indexedTypes', indexedTypes, 'java.lang.Class')
+            .intProperty('queryDetailMetricsSize')
+            .arrayProperty('sqlFunctionClasses', 'sqlFunctionClasses', cache.sqlFunctionClasses, 'java.lang.Class');
+
+        // Removed in ignite 2.0
+        if (available(['1.0.0', '2.0.0']))
+            ccfg.intProperty('snapshotableIndex');
+
+        ccfg.intProperty('sqlEscapeAll');
+
+        // Since ignite 2.0
+        if (available('2.0.0')) {
+            ccfg.intProperty('queryParallelism')
+                .intProperty('sqlIndexMaxInlineSize');
+        }
+
+        if (available('2.4.0') && cache.sqlOnheapCacheEnabled) {
+            ccfg.boolProperty('sqlOnheapCacheEnabled')
+                .intProperty('sqlOnheapCacheMaxSize');
+        }
+
+        ccfg.intProperty('maxQueryIteratorsCount');
+
+        return ccfg;
+    }
+
+    // Generate cache store group.
+    static cacheStore(cache, domains, available, deps, ccfg = this.cacheConfigurationBean(cache)) {
+        const kind = _.get(cache, 'cacheStoreFactory.kind');
+
+        if (kind && cache.cacheStoreFactory[kind]) {
+            let bean = null;
+
+            const storeFactory = cache.cacheStoreFactory[kind];
+
+            switch (kind) {
+                case 'CacheJdbcPojoStoreFactory':
+                    bean = new Bean('org.apache.ignite.cache.store.jdbc.CacheJdbcPojoStoreFactory', 'cacheStoreFactory',
+                        storeFactory, cacheDflts.cacheStoreFactory.CacheJdbcPojoStoreFactory);
+
+                    const jdbcId = bean.valueOf('dataSourceBean');
+
+                    bean.dataSource(jdbcId, 'dataSourceBean', this.dataSourceBean(jdbcId, storeFactory.dialect, available, deps, storeFactory.implementationVersion))
+                        .beanProperty('dialect', new EmptyBean(this.dialectClsName(storeFactory.dialect)));
+
+                    bean.intProperty('batchSize')
+                        .intProperty('maximumPoolSize')
+                        .intProperty('maximumWriteAttempts')
+                        .intProperty('parallelLoadCacheMinimumThreshold')
+                        .emptyBeanProperty('hasher')
+                        .emptyBeanProperty('transformer')
+                        .boolProperty('sqlEscapeAll');
+
+                    const setType = (typeBean, propName) => {
+                        if (javaTypes.nonBuiltInClass(typeBean.valueOf(propName)))
+                            typeBean.stringProperty(propName);
+                        else
+                            typeBean.classProperty(propName);
+                    };
+
+                    const types = _.reduce(domains, (acc, domain) => {
+                        if (isNil(domain.databaseTable))
+                            return acc;
+
+                        const typeBean = this.domainJdbcTypeBean(_.merge({}, domain, {cacheName: cache.name}))
+                            .stringProperty('cacheName');
+
+                        setType(typeBean, 'keyType');
+                        setType(typeBean, 'valueType');
+
+                        this.domainStore(domain, typeBean);
+
+                        acc.push(typeBean);
+
+                        return acc;
+                    }, []);
+
+                    bean.varArgProperty('types', 'types', types, 'org.apache.ignite.cache.store.jdbc.JdbcType');
+
+                    break;
+                case 'CacheJdbcBlobStoreFactory':
+                    bean = new Bean('org.apache.ignite.cache.store.jdbc.CacheJdbcBlobStoreFactory', 'cacheStoreFactory',
+                        storeFactory);
+
+                    if (bean.valueOf('connectVia') === 'DataSource') {
+                        const blobId = bean.valueOf('dataSourceBean');
+
+                        bean.dataSource(blobId, 'dataSourceBean', this.dataSourceBean(blobId, storeFactory.dialect, available, deps));
+                    }
+                    else {
+                        ccfg.stringProperty('connectionUrl')
+                            .stringProperty('user')
+                            .property('password', `ds.${storeFactory.user}.password`, 'YOUR_PASSWORD');
+                    }
+
+                    bean.boolProperty('initSchema')
+                        .stringProperty('createTableQuery')
+                        .stringProperty('loadQuery')
+                        .stringProperty('insertQuery')
+                        .stringProperty('updateQuery')
+                        .stringProperty('deleteQuery');
+
+                    break;
+                case 'CacheHibernateBlobStoreFactory':
+                    bean = new Bean('org.apache.ignite.cache.store.hibernate.CacheHibernateBlobStoreFactory',
+                        'cacheStoreFactory', storeFactory);
+
+                    bean.propsProperty('props', 'hibernateProperties');
+
+                    break;
+                default:
+            }
+
+            if (bean)
+                ccfg.beanProperty('cacheStoreFactory', bean);
+        }
+
+        ccfg.intProperty('storeConcurrentLoadAllThreshold')
+            .boolProperty('storeKeepBinary')
+            .boolProperty('loadPreviousValue')
+            .boolProperty('readThrough')
+            .boolProperty('writeThrough');
+
+        if (ccfg.valueOf('writeBehindEnabled')) {
+            ccfg.boolProperty('writeBehindEnabled')
+                .intProperty('writeBehindBatchSize')
+                .intProperty('writeBehindFlushSize')
+                .longProperty('writeBehindFlushFrequency')
+                .intProperty('writeBehindFlushThreadCount');
+
+            // Since ignite 2.0
+            if (available('2.0.0'))
+                ccfg.boolProperty('writeBehindCoalescing');
+        }
+
+        return ccfg;
+    }
+
+    // Generate cache concurrency control group.
+    static cacheConcurrency(cache, available, ccfg = this.cacheConfigurationBean(cache)) {
+        ccfg.intProperty('maxConcurrentAsyncOperations')
+            .longProperty('defaultLockTimeout');
+
+        // Removed in ignite 2.0
+        if (available(['1.0.0', '2.0.0']))
+            ccfg.enumProperty('atomicWriteOrderMode');
+
+        ccfg.enumProperty('writeSynchronizationMode');
+
+        return ccfg;
+    }
+
+    static nodeFilter(filter, igfss) {
+        const kind = _.get(filter, 'kind');
+
+        const settings = _.get(filter, kind);
+
+        if (!isNil(settings)) {
+            switch (kind) {
+                case 'IGFS':
+                    const foundIgfs = _.find(igfss, {_id: settings.igfs});
+
+                    if (foundIgfs) {
+                        return new Bean('org.apache.ignite.internal.processors.igfs.IgfsNodePredicate', 'nodeFilter', foundIgfs)
+                            .stringConstructorArgument('name');
+                    }
+
+                    break;
+                case 'Custom':
+                    if (nonEmpty(settings.className))
+                        return new EmptyBean(settings.className);
+
+                    break;
+                default:
+                // No-op.
+            }
+        }
+
+        return null;
+    }
+
+    // Generate cache node filter group.
+    static cacheNodeFilter(cache, igfss, ccfg = this.cacheConfigurationBean(cache)) {
+        const filter = _.get(cache, 'nodeFilter');
+
+        const filterBean = this.nodeFilter(filter, igfss);
+
+        if (filterBean)
+            ccfg.beanProperty('nodeFilter', filterBean);
+
+        return ccfg;
+    }
+
+    // Generate cache rebalance group.
+    static cacheRebalance(cache, ccfg = this.cacheConfigurationBean(cache)) {
+        if (ccfg.valueOf('cacheMode') !== 'LOCAL') {
+            ccfg.enumProperty('rebalanceMode')
+                .intProperty('rebalanceBatchSize')
+                .longProperty('rebalanceBatchesPrefetchCount')
+                .intProperty('rebalanceOrder')
+                .longProperty('rebalanceDelay')
+                .longProperty('rebalanceTimeout')
+                .longProperty('rebalanceThrottle');
+        }
+
+        if (ccfg.includes('igfsAffinnityGroupSize')) {
+            const bean = new Bean('org.apache.ignite.igfs.IgfsGroupDataBlocksKeyMapper', 'affinityMapper', cache)
+                .intConstructorArgument('igfsAffinnityGroupSize');
+
+            ccfg.beanProperty('affinityMapper', bean);
+        }
+
+        return ccfg;
+    }
+
+    // Generate miscellaneous configuration.
+    static cacheMisc(cache, available, cfg = this.cacheConfigurationBean(cache)) {
+        if (cache.interceptor)
+            cfg.beanProperty('interceptor', new EmptyBean(cache.interceptor));
+
+        if (available('2.0.0'))
+            cfg.boolProperty('storeByValue');
+
+        cfg.boolProperty('eagerTtl');
+
+        if (available('2.7.0'))
+            cfg.boolProperty('encryptionEnabled');
+
+        if (available('2.5.0'))
+            cfg.boolProperty('eventsDisabled');
+
+        if (cache.cacheStoreSessionListenerFactories) {
+            const factories = _.map(cache.cacheStoreSessionListenerFactories, (factory) => new EmptyBean(factory));
+
+            cfg.varArgProperty('cacheStoreSessionListenerFactories', 'cacheStoreSessionListenerFactories', factories, 'javax.cache.configuration.Factory');
+        }
+
+        return cfg;
+    }
+
+    // Generate server near cache group.
+    static cacheNearServer(cache, available, ccfg = this.cacheConfigurationBean(cache)) {
+        if (ccfg.valueOf('cacheMode') === 'PARTITIONED' && _.get(cache, 'nearConfiguration.enabled')) {
+            const bean = new Bean('org.apache.ignite.configuration.NearCacheConfiguration', 'nearConfiguration',
+                cache.nearConfiguration, cacheDflts.nearConfiguration);
+
+            bean.intProperty('nearStartSize');
+
+            this._evictionPolicy(bean, available, true, bean.valueOf('nearEvictionPolicy'), cacheDflts.evictionPolicy);
+
+            ccfg.beanProperty('nearConfiguration', bean);
+        }
+
+        return ccfg;
+    }
+
+    // Generate client near cache group.
+    static cacheNearClient(cache, available, ccfg = this.cacheConfigurationBean(cache)) {
+        if (ccfg.valueOf('cacheMode') === 'PARTITIONED' && _.get(cache, 'clientNearConfiguration.enabled')) {
+            const bean = new Bean('org.apache.ignite.configuration.NearCacheConfiguration',
+                javaTypes.toJavaName('nearConfiguration', ccfg.valueOf('name')),
+                cache.clientNearConfiguration, cacheDflts.clientNearConfiguration);
+
+            bean.intProperty('nearStartSize');
+
+            this._evictionPolicy(bean, available, true, bean.valueOf('nearEvictionPolicy'), cacheDflts.evictionPolicy);
+
+            return bean;
+        }
+
+        return ccfg;
+    }
+
+    // Generate cache statistics group.
+    static cacheStatistics(cache, ccfg = this.cacheConfigurationBean(cache)) {
+        ccfg.boolProperty('statisticsEnabled')
+            .boolProperty('managementEnabled');
+
+        return ccfg;
+    }
+
+    // Generate domain models configs.
+    static cacheDomains(domains, available, ccfg) {
+        const qryEntities = _.reduce(domains, (acc, domain) => {
+            if (isNil(domain.queryMetadata) || domain.queryMetadata === 'Configuration') {
+                const qryEntity = this.domainModelGeneral(domain);
+
+                this.domainModelQuery(domain, available, qryEntity);
+
+                acc.push(qryEntity);
+            }
+
+            return acc;
+        }, []);
+
+        ccfg.collectionProperty('qryEntities', 'queryEntities', qryEntities, 'org.apache.ignite.cache.QueryEntity');
+    }
+
+    static cacheConfiguration(cache, available, deps = [], ccfg = this.cacheConfigurationBean(cache)) {
+        this.cacheGeneral(cache, available, ccfg);
+        this.cacheAffinity(cache, available, ccfg);
+        this.cacheMemory(cache, available, ccfg);
+        this.cacheQuery(cache, cache.domains, available, ccfg);
+        this.cacheStore(cache, cache.domains, available, deps, ccfg);
+        this.cacheKeyConfiguration(cache.keyConfiguration, available, ccfg);
+
+        const igfs = _.get(cache, 'nodeFilter.IGFS.instance');
+        this.cacheNodeFilter(cache, igfs ? [igfs] : [], ccfg);
+        this.cacheConcurrency(cache, available, ccfg);
+        this.cacheRebalance(cache, ccfg);
+        this.cacheMisc(cache, available, ccfg);
+        this.cacheNearServer(cache, available, ccfg);
+        this.cacheStatistics(cache, ccfg);
+        this.cacheDomains(cache.domains, available, ccfg);
+
+        return ccfg;
+    }
+
+    // Generate IGFS general group.
+    static igfsGeneral(igfs, available, cfg = this.igfsConfigurationBean(igfs)) {
+        if (_.isEmpty(igfs.name))
+            return cfg;
+
+        cfg.stringProperty('name');
+
+        // Removed in ignite 2.0
+        if (available(['1.0.0', '2.0.0'])) {
+            cfg.stringProperty('name', 'dataCacheName', (name) => name + '-data')
+                .stringProperty('name', 'metaCacheName', (name) => name + '-meta');
+        }
+
+        cfg.enumProperty('defaultMode');
+
+        return cfg;
+    }
+
+    static _userNameMapperBean(mapper) {
+        let bean = null;
+
+        switch (mapper.kind) {
+            case 'Chained':
+                bean = new Bean('org.apache.ignite.hadoop.util.ChainedUserNameMapper', 'mameMapper', mapper.Chained);
+
+                bean.arrayProperty('mappers', 'mappers',
+                    _.filter(_.map(_.get(mapper, 'Chained.mappers'), IgniteConfigurationGenerator._userNameMapperBean), (m) => m),
+                    'org.apache.ignite.hadoop.util.UserNameMapper');
+
+                break;
+
+            case 'Basic':
+                bean = new Bean('org.apache.ignite.hadoop.util.BasicUserNameMapper', 'mameMapper', mapper.Basic, igfsDflts.secondaryFileSystem.userNameMapper.Basic);
+
+                bean.stringProperty('defaultUserName')
+                    .boolProperty('useDefaultUserName')
+                    .mapProperty('mappings', 'mappings');
+
+                break;
+
+            case 'Kerberos':
+                bean = new Bean('org.apache.ignite.hadoop.util.KerberosUserNameMapper', 'nameMapper', mapper.Kerberos);
+
+                bean.stringProperty('instance')
+                    .stringProperty('realm');
+
+                break;
+
+            case 'Custom':
+                if (_.get(mapper, 'Custom.className'))
+                    bean = new EmptyBean(mapper.Custom.className);
+
+                break;
+
+            default:
+        }
+
+        return bean;
+    }
+
+    // Generate IGFS secondary file system group.
+    static igfsSecondFS(igfs, cfg = this.igfsConfigurationBean(igfs)) {
+        if (igfs.secondaryFileSystemEnabled) {
+            const secondFs = igfs.secondaryFileSystem || {};
+
+            const bean = new Bean('org.apache.ignite.hadoop.fs.IgniteHadoopIgfsSecondaryFileSystem',
+                'secondaryFileSystem', secondFs, igfsDflts.secondaryFileSystem);
+
+            bean.stringProperty('userName', 'defaultUserName');
+
+            let factoryBean = null;
+
+            switch (secondFs.kind || 'Caching') {
+                case 'Caching':
+                    factoryBean = new Bean('org.apache.ignite.hadoop.fs.CachingHadoopFileSystemFactory', 'fac', secondFs);
+                    break;
+
+                case 'Kerberos':
+                    factoryBean = new Bean('org.apache.ignite.hadoop.fs.KerberosHadoopFileSystemFactory', 'fac', secondFs, igfsDflts.secondaryFileSystem);
+                    break;
+
+                case 'Custom':
+                    if (_.get(secondFs, 'Custom.className'))
+                        factoryBean = new Bean(secondFs.Custom.className, 'fac', null);
+
+                    break;
+
+                default:
+            }
+
+            if (!factoryBean)
+                return cfg;
+
+            if (secondFs.kind !== 'Custom') {
+                factoryBean.stringProperty('uri')
+                    .pathArrayProperty('cfgPaths', 'configPaths', secondFs.cfgPaths, true);
+
+                if (secondFs.kind === 'Kerberos') {
+                    factoryBean.stringProperty('Kerberos.keyTab', 'keyTab')
+                        .stringProperty('Kerberos.keyTabPrincipal', 'keyTabPrincipal')
+                        .longProperty('Kerberos.reloginInterval', 'reloginInterval');
+                }
+
+                if (_.get(secondFs, 'userNameMapper.kind')) {
+                    const mapper = IgniteConfigurationGenerator._userNameMapperBean(secondFs.userNameMapper);
+
+                    if (mapper)
+                        factoryBean.beanProperty('userNameMapper', mapper);
+                }
+            }
+
+            bean.beanProperty('fileSystemFactory', factoryBean);
+
+            cfg.beanProperty('secondaryFileSystem', bean);
+        }
+
+        return cfg;
+    }
+
+    // Generate IGFS IPC group.
+    static igfsIPC(igfs, cfg = this.igfsConfigurationBean(igfs)) {
+        if (igfs.ipcEndpointEnabled) {
+            const bean = new Bean('org.apache.ignite.igfs.IgfsIpcEndpointConfiguration', 'ipcEndpointConfiguration',
+                igfs.ipcEndpointConfiguration, igfsDflts.ipcEndpointConfiguration);
+
+            bean.enumProperty('type')
+                .stringProperty('host')
+                .intProperty('port')
+                .intProperty('memorySize')
+                .pathProperty('tokenDirectoryPath')
+                .intProperty('threadCount');
+
+            if (bean.nonEmpty())
+                cfg.beanProperty('ipcEndpointConfiguration', bean);
+        }
+
+        return cfg;
+    }
+
+    // Generate IGFS fragmentizer group.
+    static igfsFragmentizer(igfs, cfg = this.igfsConfigurationBean(igfs)) {
+        if (igfs.fragmentizerEnabled) {
+            cfg.intProperty('fragmentizerConcurrentFiles')
+                .longProperty('fragmentizerThrottlingBlockLength')
+                .longProperty('fragmentizerThrottlingDelay');
+        }
+        else
+            cfg.boolProperty('fragmentizerEnabled');
+
+        return cfg;
+    }
+
+    // Generate IGFS Dual mode group.
+    static igfsDualMode(igfs, cfg = this.igfsConfigurationBean(igfs)) {
+        cfg.intProperty('dualModeMaxPendingPutsSize')
+            .emptyBeanProperty('dualModePutExecutorService')
+            .intProperty('dualModePutExecutorServiceShutdown');
+
+        return cfg;
+    }
+
+    // Generate IGFS miscellaneous group.
+    static igfsMisc(igfs, available, cfg = this.igfsConfigurationBean(igfs)) {
+        cfg.intProperty('blockSize');
+
+        // Removed in ignite 2.0
+        if (available(['1.0.0', '2.0.0']))
+            cfg.intProperty('streamBufferSize');
+
+        // Since ignite 2.0
+        if (available('2.0.0'))
+            cfg.intProperty('streamBufferSize', 'bufferSize');
+
+        // Removed in ignite 2.0
+        if (available(['1.0.0', '2.0.0']))
+            cfg.intProperty('maxSpaceSize');
+
+        cfg.longProperty('maximumTaskRangeLength')
+            .intProperty('managementPort')
+            .intProperty('perNodeBatchSize')
+            .intProperty('perNodeParallelBatchCount')
+            .intProperty('prefetchBlocks')
+            .intProperty('sequentialReadsBeforePrefetch');
+
+        // Removed in ignite 2.0
+        if (available(['1.0.0', '2.0.0']))
+            cfg.intProperty('trashPurgeTimeout');
+
+        cfg.intProperty('colocateMetadata')
+            .intProperty('relaxedConsistency')
+            .mapProperty('pathModes', 'pathModes');
+
+        // Since ignite 2.0
+        if (available('2.0.0'))
+            cfg.boolProperty('updateFileLengthOnFlush');
+
+        return cfg;
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/Custom.service.js b/modules/frontend/app/configuration/generator/generator/Custom.service.js
new file mode 100644
index 0000000..a185485
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/Custom.service.js
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Optional content generation entry point.
+export default class IgniteCustomGenerator {
+    optionalContent(zip, cluster) { // eslint-disable-line no-unused-vars
+        // No-op.
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/Docker.service.js b/modules/frontend/app/configuration/generator/generator/Docker.service.js
new file mode 100644
index 0000000..8b03e9a
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/Docker.service.js
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {outdent} from 'outdent/lib';
+import VersionService from 'app/services/Version.service';
+import POM_DEPENDENCIES from 'app/data/pom-dependencies.json';
+import get from 'lodash/get';
+
+const version = new VersionService();
+
+const ALPINE_DOCKER_SINCE = '2.1.0';
+
+/**
+ * Docker file generation entry point.
+ */
+export default class IgniteDockerGenerator {
+    escapeFileName = (name) => name.replace(/[\\\/*\"\[\],\.:;|=<>?]/g, '-').replace(/ /g, '_');
+
+    /**
+     * Generate from section.
+     *
+     * @param {Object} cluster Cluster.
+     * @param {Object} targetVer Target version.
+     * @returns {String}
+     */
+    from(cluster, targetVer) {
+        return outdent`
+            # Start from Apache Ignite image.',
+            FROM apacheignite/ignite:${targetVer.ignite}
+        `;
+    }
+
+    /**
+     * Generate Docker file for cluster.
+     *
+     * @param {Object} cluster Cluster.
+     * @param {Object} targetVer Target version.
+     */
+    generate(cluster, targetVer) {
+        return outdent`
+            ${this.from(cluster, targetVer)}
+
+            # Set config uri for node.
+            ENV CONFIG_URI ${this.escapeFileName(cluster.name)}-server.xml
+
+            # Copy optional libs.
+            ENV OPTION_LIBS ${this.optionLibs(cluster, targetVer).join(',')}
+
+            # Update packages and install maven.
+            ${this.packages(cluster, targetVer)}
+            
+            # Append project to container.
+            ADD . ${cluster.name}
+
+            # Build project in container.
+            RUN mvn -f ${cluster.name}/pom.xml clean package -DskipTests
+
+            # Copy project jars to node classpath.
+            RUN mkdir $IGNITE_HOME/libs/${cluster.name} && \\
+               find ${cluster.name}/target -name "*.jar" -type f -exec cp {} $IGNITE_HOME/libs/${cluster.name} \\;
+        `;
+    }
+
+    optionLibs(cluster, targetVer) {
+        return [
+            'ignite-rest-http',
+            get(POM_DEPENDENCIES, [get(cluster, 'discovery.kind'), 'artifactId'])
+        ].filter(Boolean);
+    }
+
+    packages(cluster, targetVer) {
+        return version.since(targetVer.ignite, ALPINE_DOCKER_SINCE)
+            ? outdent`
+                RUN set -x \\
+                    && apk add --no-cache \\
+                        openjdk8
+
+                RUN apk --update add \\
+                    maven \\
+                    && rm -rfv /var/cache/apk/*
+            `
+            : outdent`
+                RUN \\
+                   apt-get update &&\\
+                   apt-get install -y maven
+            `;
+    }
+
+    ignoreFile() {
+        return outdent`
+            target
+            Dockerfile
+        `;
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/Docker.service.spec.js b/modules/frontend/app/configuration/generator/generator/Docker.service.spec.js
new file mode 100644
index 0000000..0ecada7
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/Docker.service.spec.js
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import DockerGenerator from './Docker.service';
+import {assert} from 'chai';
+import {outdent} from 'outdent/lib';
+
+suite('Dockerfile generator', () => {
+    const generator = new DockerGenerator();
+
+    test('Target 2.0', () => {
+        const cluster = {
+            name: 'FooBar'
+        };
+
+        const version = {ignite: '2.0.0'};
+
+        assert.equal(
+            generator.generate(cluster, version),
+            outdent`
+                # Start from Apache Ignite image.',
+                FROM apacheignite/ignite:2.0.0
+
+                # Set config uri for node.
+                ENV CONFIG_URI FooBar-server.xml
+
+                # Copy optional libs.
+                ENV OPTION_LIBS ignite-rest-http
+
+                # Update packages and install maven.
+                RUN \\
+                   apt-get update &&\\
+                   apt-get install -y maven
+
+                # Append project to container.
+                ADD . FooBar
+
+                # Build project in container.
+                RUN mvn -f FooBar/pom.xml clean package -DskipTests
+
+                # Copy project jars to node classpath.
+                RUN mkdir $IGNITE_HOME/libs/FooBar && \\
+                   find FooBar/target -name "*.jar" -type f -exec cp {} $IGNITE_HOME/libs/FooBar \\;
+            `
+        );
+    });
+    test('Target 2.1', () => {
+        const cluster = {
+            name: 'FooBar'
+        };
+        const version = {ignite: '2.1.0'};
+        assert.equal(
+            generator.generate(cluster, version),
+            outdent`
+                # Start from Apache Ignite image.',
+                FROM apacheignite/ignite:2.1.0
+
+                # Set config uri for node.
+                ENV CONFIG_URI FooBar-server.xml
+
+                # Copy optional libs.
+                ENV OPTION_LIBS ignite-rest-http
+
+                # Update packages and install maven.
+                RUN set -x \\
+                    && apk add --no-cache \\
+                        openjdk8
+
+                RUN apk --update add \\
+                    maven \\
+                    && rm -rfv /var/cache/apk/*
+
+                # Append project to container.
+                ADD . FooBar
+
+                # Build project in container.
+                RUN mvn -f FooBar/pom.xml clean package -DskipTests
+
+                # Copy project jars to node classpath.
+                RUN mkdir $IGNITE_HOME/libs/FooBar && \\
+                   find FooBar/target -name "*.jar" -type f -exec cp {} $IGNITE_HOME/libs/FooBar \\;
+            `
+        );
+    });
+
+    test('Discovery optional libs', () => {
+        const generateWithDiscovery = (discovery) => generator.generate({name: 'foo', discovery: {kind: discovery}}, {ignite: '2.1.0'});
+
+        assert.include(
+            generateWithDiscovery('Cloud'),
+            `ENV OPTION_LIBS ignite-rest-http,ignite-cloud`,
+            'Adds Apache jclouds lib'
+        );
+
+        assert.include(
+            generateWithDiscovery('S3'),
+            `ENV OPTION_LIBS ignite-rest-http,ignite-aws`,
+            'Adds Amazon AWS lib'
+        );
+
+        assert.include(
+            generateWithDiscovery('GoogleStorage'),
+            `ENV OPTION_LIBS ignite-rest-http,ignite-gce`,
+            'Adds Google Cloud Engine lib'
+        );
+
+        assert.include(
+            generateWithDiscovery('ZooKeeper'),
+            `ENV OPTION_LIBS ignite-rest-http,ignite-zookeeper`,
+            'Adds Zookeeper lib'
+        );
+
+        assert.include(
+            generateWithDiscovery('Kubernetes'),
+            `ENV OPTION_LIBS ignite-rest-http,ignite-kubernetes`,
+            'Adds Kubernetes lib'
+        );
+    });
+});
diff --git a/modules/frontend/app/configuration/generator/generator/JavaTransformer.service.js b/modules/frontend/app/configuration/generator/generator/JavaTransformer.service.js
new file mode 100644
index 0000000..a9df3d2
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/JavaTransformer.service.js
@@ -0,0 +1,1808 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {nonEmpty} from 'app/utils/lodashMixins';
+
+import {Bean} from './Beans';
+
+import AbstractTransformer from './AbstractTransformer';
+import StringBuilder from './StringBuilder';
+import VersionService from 'app/services/Version.service';
+
+const versionService = new VersionService();
+const STORE_FACTORY = ['org.apache.ignite.cache.store.jdbc.CacheJdbcPojoStoreFactory'];
+
+// Descriptors for generation of demo data.
+const PREDEFINED_QUERIES = [
+    {
+        schema: 'CARS',
+        type: 'PARKING',
+        create: [
+            'CREATE TABLE IF NOT EXISTS CARS.PARKING (',
+            'ID       INTEGER     NOT NULL PRIMARY KEY,',
+            'NAME     VARCHAR(50) NOT NULL,',
+            'CAPACITY INTEGER NOT NULL)'
+        ],
+        clearQuery: ['DELETE FROM CARS.PARKING'],
+        insertCntConsts: [{name: 'DEMO_MAX_PARKING_CNT', val: 5, comment: 'How many parkings to generate.'}],
+        insertPattern: ['INSERT INTO CARS.PARKING(ID, NAME, CAPACITY) VALUES(?, ?, ?)'],
+        fillInsertParameters(sb) {
+            sb.append('stmt.setInt(1, id);');
+            sb.append('stmt.setString(2, "Parking #" + (id + 1));');
+            sb.append('stmt.setInt(3, 10 + rnd.nextInt(20));');
+        },
+        selectQuery: ['SELECT * FROM PARKING WHERE CAPACITY >= 20']
+    },
+    {
+        schema: 'CARS',
+        type: 'CAR',
+        create: [
+            'CREATE TABLE IF NOT EXISTS CARS.CAR (',
+            'ID         INTEGER NOT NULL PRIMARY KEY,',
+            'PARKING_ID INTEGER NOT NULL,',
+            'NAME       VARCHAR(50) NOT NULL);'
+        ],
+        clearQuery: ['DELETE FROM CARS.CAR'],
+        rndRequired: true,
+        insertCntConsts: [
+            {name: 'DEMO_MAX_CAR_CNT', val: 10, comment: 'How many cars to generate.'},
+            {name: 'DEMO_MAX_PARKING_CNT', val: 5, comment: 'How many parkings to generate.'}
+        ],
+        insertPattern: ['INSERT INTO CARS.CAR(ID, PARKING_ID, NAME) VALUES(?, ?, ?)'],
+        fillInsertParameters(sb) {
+            sb.append('stmt.setInt(1, id);');
+            sb.append('stmt.setInt(2, rnd.nextInt(DEMO_MAX_PARKING_CNT));');
+            sb.append('stmt.setString(3, "Car #" + (id + 1));');
+        },
+        selectQuery: ['SELECT * FROM CAR WHERE PARKINGID = 2']
+    },
+    {
+        type: 'COUNTRY',
+        create: [
+            'CREATE TABLE IF NOT EXISTS COUNTRY (',
+            'ID         INTEGER NOT NULL PRIMARY KEY,',
+            'NAME       VARCHAR(50),',
+            'POPULATION INTEGER NOT NULL);'
+        ],
+        clearQuery: ['DELETE FROM COUNTRY'],
+        insertCntConsts: [{name: 'DEMO_MAX_COUNTRY_CNT', val: 5, comment: 'How many countries to generate.'}],
+        insertPattern: ['INSERT INTO COUNTRY(ID, NAME, POPULATION) VALUES(?, ?, ?)'],
+        fillInsertParameters(sb) {
+            sb.append('stmt.setInt(1, id);');
+            sb.append('stmt.setString(2, "Country #" + (id + 1));');
+            sb.append('stmt.setInt(3, 10000000 + rnd.nextInt(100000000));');
+        },
+        selectQuery: ['SELECT * FROM COUNTRY WHERE POPULATION BETWEEN 15000000 AND 25000000']
+    },
+    {
+        type: 'DEPARTMENT',
+        create: [
+            'CREATE TABLE IF NOT EXISTS DEPARTMENT (',
+            'ID         INTEGER NOT NULL PRIMARY KEY,',
+            'COUNTRY_ID INTEGER NOT NULL,',
+            'NAME       VARCHAR(50) NOT NULL);'
+        ],
+        clearQuery: ['DELETE FROM DEPARTMENT'],
+        rndRequired: true,
+        insertCntConsts: [
+            {name: 'DEMO_MAX_DEPARTMENT_CNT', val: 5, comment: 'How many departments to generate.'},
+            {name: 'DEMO_MAX_COUNTRY_CNT', val: 5, comment: 'How many countries to generate.'}
+        ],
+        insertPattern: ['INSERT INTO DEPARTMENT(ID, COUNTRY_ID, NAME) VALUES(?, ?, ?)'],
+        fillInsertParameters(sb) {
+            sb.append('stmt.setInt(1, id);');
+            sb.append('stmt.setInt(2, rnd.nextInt(DEMO_MAX_COUNTRY_CNT));');
+            sb.append('stmt.setString(3, "Department #" + (id + 1));');
+        },
+        selectQuery: ['SELECT * FROM DEPARTMENT']
+    },
+    {
+        type: 'EMPLOYEE',
+        create: [
+            'CREATE TABLE IF NOT EXISTS EMPLOYEE (',
+            'ID            INTEGER NOT NULL PRIMARY KEY,',
+            'DEPARTMENT_ID INTEGER NOT NULL,',
+            'MANAGER_ID    INTEGER,',
+            'FIRST_NAME    VARCHAR(50) NOT NULL,',
+            'LAST_NAME     VARCHAR(50) NOT NULL,',
+            'EMAIL         VARCHAR(50) NOT NULL,',
+            'PHONE_NUMBER  VARCHAR(50),',
+            'HIRE_DATE     DATE        NOT NULL,',
+            'JOB           VARCHAR(50) NOT NULL,',
+            'SALARY        DOUBLE);'
+        ],
+        clearQuery: ['DELETE FROM EMPLOYEE'],
+        rndRequired: true,
+        insertCntConsts: [
+            {name: 'DEMO_MAX_EMPLOYEE_CNT', val: 10, comment: 'How many employees to generate.'},
+            {name: 'DEMO_MAX_DEPARTMENT_CNT', val: 5, comment: 'How many departments to generate.'}
+        ],
+        customGeneration(sb, conVar, stmtVar) {
+            sb.append(`${stmtVar} = ${conVar}.prepareStatement("INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, MANAGER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");`);
+
+            sb.emptyLine();
+
+            sb.startBlock('for (int id = 0; id < DEMO_MAX_EMPLOYEE_CNT; id ++) {');
+
+            sb.append('int depId = rnd.nextInt(DEMO_MAX_DEPARTMENT_CNT);');
+
+            sb.emptyLine();
+
+            sb.append('stmt.setInt(1, DEMO_MAX_DEPARTMENT_CNT + id);');
+            sb.append('stmt.setInt(2, depId);');
+            sb.append('stmt.setInt(3, depId);');
+            sb.append('stmt.setString(4, "First name manager #" + (id + 1));');
+            sb.append('stmt.setString(5, "Last name manager#" + (id + 1));');
+            sb.append('stmt.setString(6, "Email manager#" + (id + 1));');
+            sb.append('stmt.setString(7, "Phone number manager#" + (id + 1));');
+            sb.append('stmt.setString(8, "2014-01-01");');
+            sb.append('stmt.setString(9, "Job manager #" + (id + 1));');
+            sb.append('stmt.setDouble(10, 600.0 + rnd.nextInt(300));');
+
+            sb.emptyLine();
+
+            sb.append('stmt.executeUpdate();');
+
+            sb.endBlock('}');
+        },
+        selectQuery: ['SELECT * FROM EMPLOYEE WHERE SALARY > 700']
+    }
+];
+
+// Var name generator function.
+const beanNameSeed = () => {
+    let idx = '';
+    const names = [];
+
+    return (bean) => {
+        let name;
+
+        while (_.includes(names, name = `${bean.id}${idx ? '_' + idx : idx}`))
+            idx++;
+
+        names.push(name);
+
+        return name;
+    };
+};
+
+export default class IgniteJavaTransformer extends AbstractTransformer {
+    // Mapping for objects to method call.
+    static METHOD_MAPPING = {
+        'org.apache.ignite.configuration.CacheConfiguration': {
+            prefix: 'cache',
+            name: 'name',
+            args: '',
+            generator: (sb, id, ccfg) => {
+                const cacheName = ccfg.findProperty('name').value;
+                const dataSources = IgniteJavaTransformer.collectDataSources(ccfg);
+
+                const javadoc = [
+                    `Create configuration for cache "${cacheName}".`,
+                    '',
+                    '@return Configured cache.'
+                ];
+
+                if (dataSources.length)
+                    javadoc.push('@throws Exception if failed to create cache configuration.');
+
+                IgniteJavaTransformer.commentBlock(sb, ...javadoc);
+                sb.startBlock(`public static CacheConfiguration ${id}()${dataSources.length ? ' throws Exception' : ''} {`);
+
+                IgniteJavaTransformer.constructBean(sb, ccfg, [], true);
+
+                sb.emptyLine();
+                sb.append(`return ${ccfg.id};`);
+
+                sb.endBlock('}');
+
+                return sb;
+            }
+        },
+        'org.apache.ignite.cache.store.jdbc.JdbcType': {
+            prefix: 'jdbcType',
+            name: 'valueType',
+            args: 'ccfg.getName()',
+            generator: (sb, name, jdbcType) => {
+                const javadoc = [
+                    `Create JDBC type for "${name}".`,
+                    '',
+                    '@param cacheName Cache name.',
+                    '@return Configured JDBC type.'
+                ];
+
+                IgniteJavaTransformer.commentBlock(sb, ...javadoc);
+                sb.startBlock(`private static JdbcType ${name}(String cacheName) {`);
+
+                const cacheName = jdbcType.findProperty('cacheName');
+
+                cacheName.clsName = 'var';
+                cacheName.value = 'cacheName';
+
+                IgniteJavaTransformer.constructBean(sb, jdbcType);
+
+                sb.emptyLine();
+                sb.append(`return ${jdbcType.id};`);
+
+                sb.endBlock('}');
+
+                return sb;
+            }
+        }
+    };
+
+    // Append comment line.
+    static comment(sb, ...lines) {
+        _.forEach(lines, (line) => sb.append(`// ${line}`));
+    }
+
+    // Append comment block.
+    static commentBlock(sb, ...lines) {
+        if (lines.length === 1)
+            sb.append(`/** ${_.head(lines)} **/`);
+        else {
+            sb.append('/**');
+
+            _.forEach(lines, (line) => sb.append(` * ${line}`));
+
+            sb.append(' **/');
+        }
+    }
+
+    /**
+     * @param {Bean} bean
+     */
+    static _newBean(bean) {
+        const shortClsName = this.javaTypes.shortClassName(bean.clsName);
+
+        if (_.isEmpty(bean.arguments))
+            return `new ${shortClsName}()`;
+
+        const args = _.map(bean.arguments, (arg) => {
+            switch (arg.clsName) {
+                case 'MAP':
+                    return arg.id;
+                case 'BEAN':
+                    return this._newBean(arg.value);
+                default:
+                    return this._toObject(arg.clsName, arg.value);
+            }
+        });
+
+        if (bean.factoryMtd)
+            return `${shortClsName}.${bean.factoryMtd}(${args.join(', ')})`;
+
+        return `new ${shortClsName}(${args.join(', ')})`;
+    }
+
+    /**
+     * @param {StringBuilder} sb
+     * @param {String} parentId
+     * @param {String} propertyName
+     * @param {String} value
+     * @private
+     */
+    static _setProperty(sb, parentId, propertyName, value) {
+        sb.append(`${parentId}.set${_.upperFirst(propertyName)}(${value});`);
+    }
+
+    /**
+     * @param {StringBuilder} sb
+     * @param {Array.<String>} vars
+     * @param {Boolean} limitLines
+     * @param {Bean} bean
+     * @param {String} id
+
+     * @private
+     */
+    static constructBean(sb, bean, vars = [], limitLines = false, id = bean.id) {
+        _.forEach(bean.arguments, (arg) => {
+            switch (arg.clsName) {
+                case 'MAP':
+                    this._constructMap(sb, arg, vars);
+
+                    sb.emptyLine();
+
+                    break;
+
+                default:
+                    if (this._isBean(arg.clsName) && arg.value.isComplex()) {
+                        this.constructBean(sb, arg.value, vars, limitLines);
+
+                        sb.emptyLine();
+                    }
+            }
+        });
+
+        const clsName = this.javaTypes.shortClassName(bean.clsName);
+
+        sb.append(`${this.varInit(clsName, id, vars)} = ${this._newBean(bean)};`);
+
+        if (nonEmpty(bean.properties)) {
+            sb.emptyLine();
+
+            this._setProperties(sb, bean, vars, limitLines, id);
+        }
+    }
+
+    /**
+     * @param {StringBuilder} sb
+     * @param {Bean} bean
+     * @param {Array.<String>} vars
+     * @param {Boolean} limitLines
+     * @private
+     */
+    static constructStoreFactory(sb, bean, vars, limitLines = false) {
+        const shortClsName = this.javaTypes.shortClassName(bean.clsName);
+
+        if (_.includes(vars, bean.id))
+            sb.append(`${bean.id} = ${this._newBean(bean)};`);
+        else {
+            vars.push(bean.id);
+
+            sb.append(`${shortClsName} ${bean.id} = ${this._newBean(bean)};`);
+        }
+
+        sb.emptyLine();
+
+        sb.startBlock(`${bean.id}.setDataSourceFactory(new Factory<DataSource>() {`);
+        this.commentBlock(sb, '{@inheritDoc}');
+        sb.startBlock('@Override public DataSource create() {');
+
+        sb.append(`return DataSources.INSTANCE_${bean.findProperty('dataSourceBean').id};`);
+
+        sb.endBlock('};');
+        sb.endBlock('});');
+
+        const storeFactory = _.cloneDeep(bean);
+
+        _.remove(storeFactory.properties, (p) => _.includes(['dataSourceBean'], p.name));
+
+        if (storeFactory.properties.length) {
+            sb.emptyLine();
+
+            this._setProperties(sb, storeFactory, vars, limitLines);
+        }
+    }
+
+    static _isBean(clsName) {
+        return this.javaTypes.nonBuiltInClass(clsName) && this.javaTypesNonEnum.nonEnum(clsName) && _.includes(clsName, '.');
+    }
+
+    static _toObject(clsName, val) {
+        const items = _.isArray(val) ? val : [val];
+
+        if (clsName === 'EVENTS') {
+            const lastIdx = items.length - 1;
+
+            return [..._.map(items, (v, idx) => (idx === 0 ? 'new int[] {' : ' ') + v.label + (lastIdx === idx ? '}' : ''))];
+        }
+
+        return _.map(items, (item) => {
+            if (_.isNil(item))
+                return 'null';
+
+            switch (clsName) {
+                case 'byte':
+                    return `(byte) ${item}`;
+                case 'float':
+                    return `${item}f`;
+                case 'double':
+                    return `${item}`;
+                case 'long':
+                    return `${item}L`;
+                case 'java.io.Serializable':
+                case 'java.lang.String':
+                    return `"${item.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
+                case 'PATH':
+                case 'PATH_ARRAY':
+                    return `"${item.replace(/\\/g, '\\\\')}"`;
+                case 'java.lang.Class':
+                    return `${this.javaTypes.shortClassName(item)}.class`;
+                case 'java.util.UUID':
+                    return `UUID.fromString("${item}")`;
+                case 'PROPERTY':
+                    return `props.getProperty("${item}")`;
+                case 'PROPERTY_CHAR':
+                    return `props.getProperty("${item}").toCharArray()`;
+                case 'PROPERTY_INT':
+                    return `Integer.parseInt(props.getProperty("${item}"))`;
+                default:
+                    if (this._isBean(clsName) || val instanceof Bean) {
+                        if (item.isComplex())
+                            return item.id;
+
+                        return this._newBean(item);
+                    }
+
+                    if (this.javaTypesNonEnum.nonEnum(clsName))
+                        return item;
+
+                    return `${this.javaTypes.shortClassName(clsName)}.${item}`;
+            }
+        });
+    }
+
+    static _mapperId(mapper) {
+        return (item) => this.javaTypes.toJavaName(mapper.prefix, item.findProperty(mapper.name).value);
+    }
+
+    static _constructBeans(sb, type, items, vars, limitLines) {
+        if (this._isBean(type)) {
+            // Construct objects inline for preview or simple objects.
+            const mapper = this.METHOD_MAPPING[type];
+
+            const nextId = mapper ? this._mapperId(mapper) : beanNameSeed();
+
+            // Prepare objects refs.
+            return _.map(items, (item) => {
+                if (limitLines && mapper)
+                    return nextId(item) + (limitLines ? `(${mapper.args})` : '');
+
+                if (item.isComplex()) {
+                    const id = nextId(item);
+
+                    this.constructBean(sb, item, vars, limitLines, id);
+
+                    sb.emptyLine();
+
+                    return id;
+                }
+
+                return this._newBean(item);
+            });
+        }
+
+        return this._toObject(type, items);
+    }
+
+    /**
+     *
+     * @param sb
+     * @param parentId
+     * @param arrProp
+     * @param vars
+     * @param limitLines
+     * @private
+     */
+    static _setVarArg(sb, parentId, arrProp, vars, limitLines) {
+        const refs = this._constructBeans(sb, arrProp.typeClsName, arrProp.items, vars, limitLines);
+
+        // Set refs to property.
+        if (refs.length === 1)
+            this._setProperty(sb, parentId, arrProp.name, _.head(refs));
+        else {
+            sb.startBlock(`${parentId}.set${_.upperFirst(arrProp.name)}(`);
+
+            const lastIdx = refs.length - 1;
+
+            _.forEach(refs, (ref, idx) => {
+                sb.append(ref + (lastIdx !== idx ? ',' : ''));
+            });
+
+            sb.endBlock(');');
+        }
+    }
+
+    /**
+     *
+     * @param sb
+     * @param parentId
+     * @param arrProp
+     * @param vars
+     * @param limitLines
+     * @private
+     */
+    static _setArray(sb, parentId, arrProp, vars, limitLines) {
+        const refs = this._constructBeans(sb, arrProp.typeClsName, arrProp.items, vars, limitLines);
+
+        const arrType = this.javaTypes.shortClassName(arrProp.typeClsName);
+
+        // Set refs to property.
+        sb.startBlock(`${parentId}.set${_.upperFirst(arrProp.name)}(new ${arrType}[] {`);
+
+        const lastIdx = refs.length - 1;
+
+        _.forEach(refs, (ref, idx) => sb.append(ref + (lastIdx !== idx ? ',' : '')));
+
+        sb.endBlock('});');
+    }
+
+    static _constructMap(sb, map, vars = []) {
+        const keyClsName = this.javaTypes.shortClassName(map.keyClsName);
+        const valClsName = this.javaTypes.shortClassName(map.valClsName);
+
+        const genericTypeShort = map.keyClsGenericType ? this.javaTypes.shortClassName(map.keyClsGenericType) : '';
+        const keyClsGeneric = map.keyClsGenericType ?
+            map.isKeyClsGenericTypeExtended ? `<? extends ${genericTypeShort}>` : `<${genericTypeShort}>`
+            : '';
+
+        const mapClsName = map.ordered ? 'LinkedHashMap' : 'HashMap';
+
+        const type = `${mapClsName}<${keyClsName}${keyClsGeneric}, ${valClsName}>`;
+
+        sb.append(`${this.varInit(type, map.id, vars)} = new ${mapClsName}<>();`);
+
+        sb.emptyLine();
+
+        _.forEach(map.entries, (entry) => {
+            const key = this._toObject(map.keyClsName, entry[map.keyField]);
+            const val = entry[map.valField];
+
+            if (_.isArray(val) && map.valClsName === 'java.lang.String') {
+                if (val.length > 1) {
+                    sb.startBlock(`${map.id}.put(${key},`);
+
+                    _.forEach(val, (line, idx) => {
+                        sb.append(`"${line}"${idx !== val.length - 1 ? ' +' : ''}`);
+                    });
+
+                    sb.endBlock(');');
+                }
+                else
+                    sb.append(`${map.id}.put(${key}, ${this._toObject(map.valClsName, _.head(val))});`);
+            }
+            else
+                sb.append(`${map.id}.put(${key}, ${this._toObject(map.valClsNameShow || map.valClsName, val)});`);
+        });
+    }
+
+    static varInit(type, id, vars) {
+        if (_.includes(vars, id))
+            return id;
+
+        vars.push(id);
+
+        return `${type} ${id}`;
+    }
+
+    /**
+     *
+     * @param {StringBuilder} sb
+     * @param {Bean} bean
+     * @param {String} id
+     * @param {Array.<String>} vars
+     * @param {Boolean} limitLines
+     * @returns {StringBuilder}
+     */
+    static _setProperties(sb = new StringBuilder(), bean, vars = [], limitLines = false, id = bean.id) {
+        _.forEach(bean.properties, (prop, idx) => {
+            switch (prop.clsName) {
+                case 'DATA_SOURCE':
+                    this._setProperty(sb, id, 'dataSource', `DataSources.INSTANCE_${prop.id}`);
+
+                    break;
+                case 'EVENT_TYPES':
+                    if (prop.eventTypes.length === 1) {
+                        const evtGrp = _.head(prop.eventTypes);
+
+                        this._setProperty(sb, id, prop.name, evtGrp.label);
+                    }
+                    else {
+                        const evtGrp = _.map(prop.eventTypes, 'label');
+
+                        sb.append(`int[] ${prop.id} = new int[${_.head(evtGrp)}.length`);
+
+                        _.forEach(_.tail(evtGrp), (evtGrp) => {
+                            sb.append(`    + ${evtGrp}.length`);
+                        });
+
+                        sb.append('];');
+
+                        sb.emptyLine();
+
+                        sb.append('int k = 0;');
+
+                        _.forEach(evtGrp, (evtGrp, evtIdx) => {
+                            sb.emptyLine();
+
+                            sb.append(`System.arraycopy(${evtGrp}, 0, ${prop.id}, k, ${evtGrp}.length);`);
+
+                            if (evtIdx < evtGrp.length - 1)
+                                sb.append(`k += ${evtGrp}.length;`);
+                        });
+
+                        sb.emptyLine();
+
+                        sb.append(`cfg.setIncludeEventTypes(${prop.id});`);
+                    }
+
+                    break;
+                case 'ARRAY':
+                    if (prop.varArg)
+                        this._setVarArg(sb, id, prop, vars, limitLines);
+                    else
+                        this._setArray(sb, id, prop, vars, limitLines);
+
+                    break;
+                case 'PATH_ARRAY':
+                    if (prop.varArg)
+                        this._setVarArg(sb, id, prop, this._toObject(prop.clsName, prop.items), limitLines);
+                    else
+                        this._setArray(sb, id, prop, this._toObject(prop.clsName, prop.items), limitLines);
+
+                    break;
+                case 'COLLECTION':
+                    const nonBean = !this._isBean(prop.typeClsName);
+
+                    if (nonBean && prop.implClsName === 'java.util.ArrayList') {
+                        const items = _.map(prop.items, (item) => this._toObject(prop.typeClsName, item));
+
+                        if (items.length > 1) {
+                            sb.startBlock(`${id}.set${_.upperFirst(prop.name)}(Arrays.asList(`);
+
+                            _.forEach(items, (item, i) => sb.append(item + (i !== items.length - 1 ? ',' : '')));
+
+                            sb.endBlock('));');
+                        }
+                        else
+                            this._setProperty(sb, id, prop.name, `Arrays.asList(${items})`);
+                    }
+                    else {
+                        const colTypeClsName = this.javaTypes.shortClassName(prop.typeClsName);
+                        const implClsName = this.javaTypes.shortClassName(prop.implClsName);
+
+                        sb.append(`${this.varInit(`${implClsName}<${colTypeClsName}>`, prop.id, vars)} = new ${implClsName}<>();`);
+
+                        sb.emptyLine();
+
+                        if (nonBean) {
+                            _.forEach(this._toObject(colTypeClsName, prop.items), (item) => {
+                                if (this.javaTypesNonEnum.nonEnum(prop.typeClsName))
+                                    sb.append(`${prop.id}.add("${item}");`);
+                                else
+                                    sb.append(`${prop.id}.add(${item});`);
+
+                                sb.emptyLine();
+                            });
+                        }
+                        else {
+                            _.forEach(prop.items, (item) => {
+                                this.constructBean(sb, item, vars, limitLines);
+
+                                sb.append(`${prop.id}.add(${item.id});`);
+
+                                sb.emptyLine();
+                            });
+                        }
+
+                        this._setProperty(sb, id, prop.name, prop.id);
+                    }
+
+                    break;
+                case 'MAP':
+                    this._constructMap(sb, prop, vars);
+
+                    if (nonEmpty(prop.entries))
+                        sb.emptyLine();
+
+                    this._setProperty(sb, id, prop.name, prop.id);
+
+                    break;
+                case 'java.util.Properties':
+                    sb.append(`${this.varInit('Properties', prop.id, vars)} = new Properties();`);
+
+                    if (nonEmpty(prop.entries))
+                        sb.emptyLine();
+
+                    _.forEach(prop.entries, (entry) => {
+                        const key = this._toObject('java.lang.String', entry.name);
+                        const val = this._toObject('java.lang.String', entry.value);
+
+                        sb.append(`${prop.id}.setProperty(${key}, ${val});`);
+                    });
+
+                    sb.emptyLine();
+
+                    this._setProperty(sb, id, prop.name, prop.id);
+
+                    break;
+                case 'BEAN':
+                    const embedded = prop.value;
+
+                    if (_.includes(STORE_FACTORY, embedded.clsName)) {
+                        this.constructStoreFactory(sb, embedded, vars, limitLines);
+
+                        sb.emptyLine();
+
+                        this._setProperty(sb, id, prop.name, embedded.id);
+                    }
+                    else if (embedded.isComplex()) {
+                        this.constructBean(sb, embedded, vars, limitLines);
+
+                        sb.emptyLine();
+
+                        this._setProperty(sb, id, prop.name, embedded.id);
+                    }
+                    else
+                        this._setProperty(sb, id, prop.name, this._newBean(embedded));
+
+                    break;
+                default:
+                    this._setProperty(sb, id, prop.name, this._toObject(prop.clsName, prop.value));
+            }
+
+            this._emptyLineIfNeeded(sb, bean.properties, idx);
+        });
+
+        return sb;
+    }
+
+    static _collectMapImports(prop) {
+        const imports = [];
+
+        imports.push(prop.ordered ? 'java.util.LinkedHashMap' : 'java.util.HashMap');
+        imports.push(prop.keyClsName);
+        imports.push(prop.valClsName);
+
+        if (prop.keyClsGenericType)
+            imports.push(prop.keyClsGenericType);
+
+        return imports;
+    }
+
+    static collectBeanImports(bean) {
+        const imports = [bean.clsName];
+
+        _.forEach(bean.arguments, (arg) => {
+            switch (arg.clsName) {
+                case 'BEAN':
+                    imports.push(...this.collectPropertiesImports(arg.value.properties));
+
+                    break;
+                case 'java.lang.Class':
+                    imports.push(this.javaTypes.fullClassName(arg.value));
+
+                    break;
+
+                case 'MAP':
+                    imports.push(...this._collectMapImports(arg));
+
+                    break;
+                default:
+                    imports.push(arg.clsName);
+            }
+        });
+
+        imports.push(...this.collectPropertiesImports(bean.properties));
+
+        if (_.includes(STORE_FACTORY, bean.clsName))
+            imports.push('javax.sql.DataSource', 'javax.cache.configuration.Factory');
+
+        return imports;
+    }
+
+    /**
+     * @param {Array.<Object>} props
+     * @returns {Array.<String>}
+     */
+    static collectPropertiesImports(props) {
+        const imports = [];
+
+        _.forEach(props, (prop) => {
+            switch (prop.clsName) {
+                case 'DATA_SOURCE':
+                    imports.push(prop.value.clsName);
+
+                    break;
+                case 'PROPERTY':
+                case 'PROPERTY_CHAR':
+                case 'PROPERTY_INT':
+                    imports.push('java.io.InputStream', 'java.util.Properties');
+
+                    break;
+                case 'BEAN':
+                    imports.push(...this.collectBeanImports(prop.value));
+
+                    break;
+                case 'ARRAY':
+                    if (!prop.varArg)
+                        imports.push(prop.typeClsName);
+
+                    if (this._isBean(prop.typeClsName))
+                        _.forEach(prop.items, (item) => imports.push(...this.collectBeanImports(item)));
+
+                    if (prop.typeClsName === 'java.lang.Class')
+                        _.forEach(prop.items, (item) => imports.push(item));
+
+                    break;
+                case 'COLLECTION':
+                    imports.push(prop.typeClsName);
+
+                    if (this._isBean(prop.typeClsName)) {
+                        _.forEach(prop.items, (item) => imports.push(...this.collectBeanImports(item)));
+
+                        imports.push(prop.implClsName);
+                    }
+                    else if (prop.implClsName === 'java.util.ArrayList')
+                        imports.push('java.util.Arrays');
+                    else
+                        imports.push(prop.implClsName);
+
+                    break;
+                case 'MAP':
+                    imports.push(...this._collectMapImports(prop));
+
+                    break;
+                default:
+                    if (!this.javaTypesNonEnum.nonEnum(prop.clsName))
+                        imports.push(prop.clsName);
+            }
+        });
+
+        return imports;
+    }
+
+    static _prepareImports(imports) {
+        return _.sortedUniq(_.sortBy(_.filter(imports, (cls) => !_.startsWith(cls, 'java.lang.') && _.includes(cls, '.'))));
+    }
+
+    /**
+     * @param {Bean} bean
+     * @returns {Array.<String>}
+     */
+    static collectStaticImports(bean) {
+        const imports = [];
+
+        _.forEach(bean.properties, (prop) => {
+            switch (prop.clsName) {
+                case 'EVENT_TYPES':
+                    _.forEach(prop.eventTypes, (grp) => {
+                        imports.push(`${grp.class}.${grp.value}`);
+                    });
+
+                    break;
+
+                case 'MAP':
+                    if (prop.valClsNameShow === 'EVENTS') {
+                        _.forEach(prop.entries, (lnr) => {
+                            _.forEach(lnr.eventTypes, (type) => imports.push(`${type.class}.${type.label}`));
+                        });
+                    }
+
+                    break;
+
+                default:
+                    // No-op.
+            }
+        });
+
+        return imports;
+    }
+
+    /**
+     * @param {Bean} bean
+     * @returns {Object}
+     */
+    static collectBeansWithMapping(bean) {
+        const beans = {};
+
+        _.forEach(bean.properties, (prop) => {
+            switch (prop.clsName) {
+                case 'BEAN':
+                    _.merge(beans, this.collectBeansWithMapping(prop.value));
+
+                    break;
+                case 'ARRAY':
+                    if (this._isBean(prop.typeClsName)) {
+                        const mapper = this.METHOD_MAPPING[prop.typeClsName];
+
+                        const mapperId = mapper ? this._mapperId(mapper) : null;
+
+                        _.reduce(prop.items, (acc, item) => {
+                            if (mapperId)
+                                acc[mapperId(item)] = item;
+
+                            _.merge(acc, this.collectBeansWithMapping(item));
+
+                            return acc;
+                        }, beans);
+                    }
+
+                    break;
+                default:
+                    // No-op.
+            }
+        });
+
+        return beans;
+    }
+
+    /**
+     * Build Java startup class with configuration.
+     *
+     * @param {Bean} cfg
+     * @param {Object} targetVer Version of Ignite for generated project.
+     * @param pkg Package name.
+     * @param {String} clsName Class name for generate factory class otherwise generate code snippet.
+     * @param {Array.<Object>} clientNearCaches Is client node.
+     * @returns {StringBuilder}
+     */
+    static igniteConfiguration(cfg, targetVer, pkg, clsName, clientNearCaches) {
+        const available = versionService.since.bind(versionService, targetVer.ignite);
+
+        const sb = new StringBuilder();
+
+        sb.append(`package ${pkg};`);
+        sb.emptyLine();
+
+        const imports = this.collectBeanImports(cfg);
+
+        const nearCacheBeans = [];
+
+        if (nonEmpty(clientNearCaches)) {
+            imports.push('org.apache.ignite.configuration.NearCacheConfiguration');
+
+            _.forEach(clientNearCaches, (cache) => {
+                const nearCacheBean = this.generator.cacheNearClient(cache, available);
+
+                nearCacheBean.cacheName = cache.name;
+
+                imports.push(...this.collectBeanImports(nearCacheBean));
+
+                nearCacheBeans.push(nearCacheBean);
+            });
+        }
+
+        if (_.includes(imports, 'oracle.jdbc.pool.OracleDataSource'))
+            imports.push('java.sql.SQLException');
+
+        const hasProps = this.hasProperties(cfg);
+
+        if (hasProps)
+            imports.push('java.util.Properties', 'java.io.InputStream');
+
+        _.forEach(this._prepareImports(imports), (cls) => sb.append(`import ${cls};`));
+
+        sb.emptyLine();
+
+        const staticImports = this._prepareImports(this.collectStaticImports(cfg));
+
+        if (staticImports.length) {
+            _.forEach(this._prepareImports(staticImports), (cls) => sb.append(`import static ${cls};`));
+
+            sb.emptyLine();
+        }
+
+        this.mainComment(sb);
+        sb.startBlock(`public class ${clsName} {`);
+
+        // 2. Add external property file
+        if (hasProps) {
+            this.commentBlock(sb, 'Secret properties loading.');
+            sb.append('private static final Properties props = new Properties();');
+            sb.emptyLine();
+            sb.startBlock('static {');
+            sb.startBlock('try (InputStream in = IgniteConfiguration.class.getClassLoader().getResourceAsStream("secret.properties")) {');
+            sb.append('props.load(in);');
+            sb.endBlock('}');
+            sb.startBlock('catch (Exception ignored) {');
+            sb.append('// No-op.');
+            sb.endBlock('}');
+            sb.endBlock('}');
+            sb.emptyLine();
+        }
+
+        // 3. Add data sources.
+        const dataSources = this.collectDataSources(cfg);
+
+        if (dataSources.length) {
+            this.commentBlock(sb, 'Helper class for datasource creation.');
+            sb.startBlock('public static class DataSources {');
+
+            _.forEach(dataSources, (ds, idx) => {
+                const dsClsName = this.javaTypes.shortClassName(ds.clsName);
+
+                if (idx !== 0)
+                    sb.emptyLine();
+
+                sb.append(`public static final ${dsClsName} INSTANCE_${ds.id} = create${ds.id}();`);
+                sb.emptyLine();
+
+                sb.startBlock(`private static ${dsClsName} create${ds.id}() {`);
+
+                if (dsClsName === 'OracleDataSource')
+                    sb.startBlock('try {');
+
+                this.constructBean(sb, ds);
+
+                sb.emptyLine();
+                sb.append(`return ${ds.id};`);
+
+                if (dsClsName === 'OracleDataSource') {
+                    sb.endBlock('}');
+                    sb.startBlock('catch (SQLException ex) {');
+                    sb.append('throw new Error(ex);');
+                    sb.endBlock('}');
+                }
+
+                sb.endBlock('}');
+            });
+
+            sb.endBlock('}');
+
+            sb.emptyLine();
+        }
+
+        _.forEach(nearCacheBeans, (nearCacheBean) => {
+            this.commentBlock(sb, `Configuration of near cache for cache: ${nearCacheBean.cacheName}.`,
+                '',
+                '@return Near cache configuration.',
+                '@throws Exception If failed to construct near cache configuration instance.'
+            );
+
+            sb.startBlock(`public static NearCacheConfiguration ${nearCacheBean.id}() throws Exception {`);
+
+            this.constructBean(sb, nearCacheBean);
+            sb.emptyLine();
+
+            sb.append(`return ${nearCacheBean.id};`);
+            sb.endBlock('}');
+
+            sb.emptyLine();
+        });
+
+        this.commentBlock(sb, 'Configure grid.',
+            '',
+            '@return Ignite configuration.',
+            '@throws Exception If failed to construct Ignite configuration instance.'
+        );
+        sb.startBlock('public static IgniteConfiguration createConfiguration() throws Exception {');
+
+        this.constructBean(sb, cfg, [], true);
+
+        sb.emptyLine();
+
+        sb.append(`return ${cfg.id};`);
+
+        sb.endBlock('}');
+
+        const beans = this.collectBeansWithMapping(cfg);
+
+        _.forEach(beans, (bean, id) => {
+            sb.emptyLine();
+
+            this.METHOD_MAPPING[bean.clsName].generator(sb, id, bean);
+        });
+
+        sb.endBlock('}');
+
+        return sb;
+    }
+
+    static cluster(cluster, targetVer, pkg, clsName, client) {
+        const cfg = this.generator.igniteConfiguration(cluster, targetVer, client);
+
+        const clientNearCaches = client ? _.filter(cluster.caches, (cache) =>
+            cache.cacheMode === 'PARTITIONED' && _.get(cache, 'clientNearConfiguration.enabled')) : [];
+
+        return this.igniteConfiguration(cfg, targetVer, pkg, clsName, clientNearCaches);
+    }
+
+    /**
+     * Generate source code for type by its domain model.
+     *
+     * @param fullClsName Full class name.
+     * @param fields Fields.
+     * @param addConstructor If 'true' then empty and full constructors should be generated.
+     * @returns {StringBuilder}
+     */
+    static pojo(fullClsName, fields, addConstructor) {
+        const dotIdx = fullClsName.lastIndexOf('.');
+
+        const pkg = fullClsName.substring(0, dotIdx);
+        const clsName = fullClsName.substring(dotIdx + 1);
+
+        const sb = new StringBuilder();
+
+        sb.append(`package ${pkg};`);
+        sb.emptyLine();
+
+        const imports = ['java.io.Serializable'];
+
+        _.forEach(fields, (field) => imports.push(this.javaTypes.fullClassName(field.javaFieldType)));
+
+        _.forEach(this._prepareImports(imports), (cls) => sb.append(`import ${cls};`));
+
+        sb.emptyLine();
+
+        this.mainComment(sb,
+            `${clsName} definition.`,
+            ''
+        );
+        sb.startBlock(`public class ${clsName} implements Serializable {`);
+        sb.append('/** */');
+        sb.append('private static final long serialVersionUID = 0L;');
+        sb.emptyLine();
+
+        // Generate fields declaration.
+        _.forEach(fields, (field) => {
+            const fldName = field.javaFieldName;
+            const fldType = this.javaTypes.shortClassName(field.javaFieldType);
+
+            sb.append(`/** Value for ${fldName}. */`);
+            sb.append(`private ${fldType} ${fldName};`);
+
+            sb.emptyLine();
+        });
+
+        // Generate constructors.
+        if (addConstructor) {
+            this.commentBlock(sb, 'Empty constructor.');
+            sb.startBlock(`public ${clsName}() {`);
+            this.comment(sb, 'No-op.');
+            sb.endBlock('}');
+
+            sb.emptyLine();
+
+            this.commentBlock(sb, 'Full constructor.');
+
+            const arg = (field) => {
+                const fldType = this.javaTypes.shortClassName(field.javaFieldType);
+
+                return `${fldType} ${field.javaFieldName}`;
+            };
+
+            sb.startBlock(`public ${clsName}(${arg(_.head(fields))}${fields.length === 1 ? ') {' : ','}`);
+
+            _.forEach(_.tail(fields), (field, idx) => {
+                sb.append(`${arg(field)}${idx !== fields.length - 2 ? ',' : ') {'}`);
+            });
+
+            _.forEach(fields, (field) => sb.append(`this.${field.javaFieldName} = ${field.javaFieldName};`));
+
+            sb.endBlock('}');
+
+            sb.emptyLine();
+        }
+
+        // Generate getters and setters methods.
+        _.forEach(fields, (field) => {
+            const fldType = this.javaTypes.shortClassName(field.javaFieldType);
+            const fldName = field.javaFieldName;
+
+            this.commentBlock(sb,
+                `Gets ${fldName}`,
+                '',
+                `@return Value for ${fldName}.`
+            );
+            sb.startBlock(`public ${fldType} ${this.javaTypes.toJavaName('get', fldName)}() {`);
+            sb.append('return ' + fldName + ';');
+            sb.endBlock('}');
+
+            sb.emptyLine();
+
+            this.commentBlock(sb,
+                `Sets ${fldName}`,
+                '',
+                `@param ${fldName} New value for ${fldName}.`
+            );
+            sb.startBlock(`public void ${this.javaTypes.toJavaName('set', fldName)}(${fldType} ${fldName}) {`);
+            sb.append(`this.${fldName} = ${fldName};`);
+            sb.endBlock('}');
+
+            sb.emptyLine();
+        });
+
+        // Generate equals() method.
+        this.commentBlock(sb, '{@inheritDoc}');
+        sb.startBlock('@Override public boolean equals(Object o) {');
+        sb.startBlock('if (this == o)');
+        sb.append('return true;');
+
+        sb.endBlock('');
+
+        sb.startBlock(`if (!(o instanceof ${clsName}))`);
+        sb.append('return false;');
+
+        sb.endBlock('');
+
+        sb.append(`${clsName} that = (${clsName})o;`);
+
+        _.forEach(fields, (field) => {
+            sb.emptyLine();
+
+            const javaName = field.javaFieldName;
+            const javaType = field.javaFieldType;
+
+            switch (javaType) {
+                case 'float':
+                    sb.startBlock(`if (Float.compare(${javaName}, that.${javaName}) != 0)`);
+
+                    break;
+                case 'double':
+                    sb.startBlock(`if (Double.compare(${javaName}, that.${javaName}) != 0)`);
+
+                    break;
+                default:
+                    if (this.javaTypes.isPrimitive(javaType))
+                        sb.startBlock('if (' + javaName + ' != that.' + javaName + ')');
+                    else
+                        sb.startBlock('if (' + javaName + ' != null ? !' + javaName + '.equals(that.' + javaName + ') : that.' + javaName + ' != null)');
+            }
+
+            sb.append('return false;');
+
+            sb.endBlock('');
+        });
+
+        sb.append('return true;');
+        sb.endBlock('}');
+
+        sb.emptyLine();
+
+        // Generate hashCode() method.
+        this.commentBlock(sb, '{@inheritDoc}');
+        sb.startBlock('@Override public int hashCode() {');
+
+        let first = true;
+        let tempVar = false;
+
+        _.forEach(fields, (field) => {
+            const javaName = field.javaFieldName;
+            const javaType = field.javaFieldType;
+
+            let fldHashCode;
+
+            switch (javaType) {
+                case 'boolean':
+                    fldHashCode = `${javaName} ? 1 : 0`;
+
+                    break;
+                case 'byte':
+                case 'short':
+                    fldHashCode = `(int)${javaName}`;
+
+                    break;
+                case 'int':
+                    fldHashCode = `${javaName}`;
+
+                    break;
+                case 'long':
+                    fldHashCode = `(int)(${javaName} ^ (${javaName} >>> 32))`;
+
+                    break;
+                case 'float':
+                    fldHashCode = `${javaName} != +0.0f ? Float.floatToIntBits(${javaName}) : 0`;
+
+                    break;
+                case 'double':
+                    sb.append(`${tempVar ? 'ig_hash_temp' : 'long ig_hash_temp'} = Double.doubleToLongBits(${javaName});`);
+
+                    tempVar = true;
+
+                    fldHashCode = '(int) (ig_hash_temp ^ (ig_hash_temp >>> 32))';
+
+                    break;
+                default:
+                    fldHashCode = `${javaName} != null ? ${javaName}.hashCode() : 0`;
+            }
+
+            sb.append(first ? `int res = ${fldHashCode};` : `res = 31 * res + ${fldHashCode.startsWith('(') ? fldHashCode : `(${fldHashCode})`};`);
+
+            first = false;
+
+            sb.emptyLine();
+        });
+
+        sb.append('return res;');
+        sb.endBlock('}');
+
+        sb.emptyLine();
+
+        this.commentBlock(sb, '{@inheritDoc}');
+        sb.startBlock('@Override public String toString() {');
+        sb.startBlock(`return "${clsName} [" + `);
+
+        _.forEach(fields, (field, idx) => {
+            sb.append(`"${field.javaFieldName}=" + ${field.javaFieldName}${idx < fields.length - 1 ? ' + ", " + ' : ' +'}`);
+        });
+
+        sb.endBlock('"]";');
+        sb.endBlock('}');
+
+        sb.endBlock('}');
+
+        return sb.asString();
+    }
+
+    /**
+     * Generate source code for type by its domain models.
+     *
+     * @param caches List of caches to generate POJOs for.
+     * @param addConstructor If 'true' then generate constructors.
+     * @param includeKeyFields If 'true' then include key fields into value POJO.
+     */
+    static pojos(caches, addConstructor, includeKeyFields) {
+        const pojos = [];
+
+        _.forEach(caches, (cache) => {
+            _.forEach(cache.domains, (domain) => {
+                // Process only  domains with 'generatePojo' flag and skip already generated classes.
+                if (domain.generatePojo && !_.find(pojos, {valueType: domain.valueType}) &&
+                    // Skip domain models without value fields.
+                    nonEmpty(domain.valueFields)) {
+                    const pojo = {
+                        keyType: domain.keyType,
+                        valueType: domain.valueType
+                    };
+
+                    // Key class generation only if key is not build in java class.
+                    if (this.javaTypes.nonBuiltInClass(domain.keyType) && nonEmpty(domain.keyFields))
+                        pojo.keyClass = this.pojo(domain.keyType, domain.keyFields, addConstructor);
+
+                    const valueFields = _.clone(domain.valueFields);
+
+                    if (includeKeyFields) {
+                        _.forEach(domain.keyFields, (fld) => {
+                            if (!_.find(valueFields, {javaFieldName: fld.javaFieldName}))
+                                valueFields.push(fld);
+                        });
+                    }
+
+                    pojo.valueClass = this.pojo(domain.valueType, valueFields, addConstructor);
+
+                    pojos.push(pojo);
+                }
+            });
+        });
+
+        return pojos;
+    }
+
+    // Generate creation and execution of cache query.
+    static _multilineQuery(sb, query, prefix, postfix) {
+        if (_.isEmpty(query))
+            return;
+
+        _.forEach(query, (line, ix) => {
+            if (ix === 0) {
+                if (query.length === 1)
+                    sb.append(`${prefix}"${line}"${postfix}`);
+                else
+                    sb.startBlock(`${prefix}"${line}" +`);
+            }
+            else
+                sb.append(`"${line}"${ix === query.length - 1 ? postfix : ' +'}`);
+        });
+
+        if (query.length > 1)
+            sb.endBlock('');
+        else
+            sb.emptyLine();
+    }
+
+    // Generate creation and execution of prepared statement.
+    static _prepareStatement(sb, conVar, query) {
+        this._multilineQuery(sb, query, `${conVar}.prepareStatement(`, ').executeUpdate();');
+    }
+
+    static demoStartup(sb, cluster, shortFactoryCls) {
+        const cachesWithDataSource = _.filter(cluster.caches, (cache) => {
+            const kind = _.get(cache, 'cacheStoreFactory.kind');
+
+            if (kind) {
+                const store = cache.cacheStoreFactory[kind];
+
+                return (store.connectVia === 'DataSource' || _.isNil(store.connectVia)) && store.dialect;
+            }
+
+            return false;
+        });
+
+        const uniqDomains = [];
+
+        // Prepare array of cache and his demo domain model list. Every domain is contained only in first cache.
+        const demoTypes = _.reduce(cachesWithDataSource, (acc, cache) => {
+            const domains = _.filter(cache.domains, (domain) => nonEmpty(domain.valueFields) &&
+                !_.includes(uniqDomains, domain));
+
+            if (nonEmpty(domains)) {
+                uniqDomains.push(...domains);
+
+                acc.push({
+                    cache,
+                    domains
+                });
+            }
+
+            return acc;
+        }, []);
+
+        if (nonEmpty(demoTypes)) {
+            // Group domain modes by data source
+            const typeByDs = _.groupBy(demoTypes, ({cache}) => cache.cacheStoreFactory[cache.cacheStoreFactory.kind].dataSourceBean);
+
+            let rndNonDefined = true;
+
+            const generatedConsts = [];
+
+            _.forEach(typeByDs, (types) => {
+                _.forEach(types, (type) => {
+                    _.forEach(type.domains, (domain) => {
+                        const valType = domain.valueType.toUpperCase();
+
+                        const desc = _.find(PREDEFINED_QUERIES, (qry) => valType.endsWith(qry.type));
+
+                        if (desc) {
+                            if (rndNonDefined && desc.rndRequired) {
+                                this.commentBlock(sb, 'Random generator for demo data.');
+                                sb.append('private static final Random rnd = new Random();');
+
+                                sb.emptyLine();
+
+                                rndNonDefined = false;
+                            }
+
+                            _.forEach(desc.insertCntConsts, (cnt) => {
+                                if (!_.includes(generatedConsts, cnt.name)) {
+                                    this.commentBlock(sb, cnt.comment);
+                                    sb.append(`private static final int ${cnt.name} = ${cnt.val};`);
+
+                                    sb.emptyLine();
+
+                                    generatedConsts.push(cnt.name);
+                                }
+                            });
+                        }
+                    });
+                });
+            });
+
+            // Generation of fill database method
+            this.commentBlock(sb, 'Fill data for Demo.');
+            sb.startBlock('private static void prepareDemoData() throws SQLException {');
+
+            let firstDs = true;
+
+            _.forEach(typeByDs, (types, ds) => {
+                const conVar = ds + 'Con';
+
+                if (firstDs)
+                    firstDs = false;
+                else
+                    sb.emptyLine();
+
+                sb.startBlock(`try (Connection ${conVar} = ${shortFactoryCls}.DataSources.INSTANCE_${ds}.getConnection()) {`);
+
+                let first = true;
+                let stmtFirst = true;
+
+                _.forEach(types, (type) => {
+                    _.forEach(type.domains, (domain) => {
+                        const valType = domain.valueType.toUpperCase();
+
+                        const desc = _.find(PREDEFINED_QUERIES, (qry) => valType.endsWith(qry.type));
+
+                        if (desc) {
+                            if (first)
+                                first = false;
+                            else
+                                sb.emptyLine();
+
+                            this.comment(sb, `Generate ${desc.type}.`);
+
+                            if (desc.schema)
+                                this._prepareStatement(sb, conVar, [`CREATE SCHEMA IF NOT EXISTS ${desc.schema}`]);
+
+                            this._prepareStatement(sb, conVar, desc.create);
+
+                            this._prepareStatement(sb, conVar, desc.clearQuery);
+
+                            let stmtVar = 'stmt';
+
+                            if (stmtFirst) {
+                                stmtFirst = false;
+
+                                stmtVar = 'PreparedStatement stmt';
+                            }
+
+                            if (_.isFunction(desc.customGeneration))
+                                desc.customGeneration(sb, conVar, stmtVar);
+                            else {
+                                sb.append(`${stmtVar} = ${conVar}.prepareStatement("${desc.insertPattern}");`);
+
+                                sb.emptyLine();
+
+                                sb.startBlock(`for (int id = 0; id < ${desc.insertCntConsts[0].name}; id ++) {`);
+
+                                desc.fillInsertParameters(sb);
+
+                                sb.emptyLine();
+
+                                sb.append('stmt.executeUpdate();');
+
+                                sb.endBlock('}');
+                            }
+
+                            sb.emptyLine();
+
+                            sb.append(`${conVar}.commit();`);
+                        }
+                    });
+                });
+
+                sb.endBlock('}');
+            });
+
+            sb.endBlock('}');
+
+            sb.emptyLine();
+
+            this.commentBlock(sb, 'Print result table to console.');
+            sb.startBlock('private static void printResult(List<Cache.Entry<Object, Object>> rows) {');
+            sb.append('for (Cache.Entry<Object, Object> row: rows)');
+            sb.append('    System.out.println(row);');
+            sb.endBlock('}');
+
+            sb.emptyLine();
+
+            // Generation of execute queries method.
+            this.commentBlock(sb, 'Run demo.');
+            sb.startBlock('private static void runDemo(Ignite ignite) throws SQLException {');
+
+            const getType = (fullType) => fullType.substr(fullType.lastIndexOf('.') + 1);
+
+            const cacheLoaded = [];
+            let rowVariableDeclared = false;
+            firstDs = true;
+
+            _.forEach(typeByDs, (types, ds) => {
+                const conVar = ds + 'Con';
+
+                if (firstDs)
+                    firstDs = false;
+                else
+                    sb.emptyLine();
+
+                sb.startBlock(`try (Connection ${conVar} = ${shortFactoryCls}.DataSources.INSTANCE_${ds}.getConnection()) {`);
+
+                let first = true;
+
+                _.forEach(types, (type) => {
+                    _.forEach(type.domains, (domain) => {
+                        const valType = domain.valueType.toUpperCase();
+
+                        const desc = _.find(PREDEFINED_QUERIES, (qry) => valType.endsWith(qry.type));
+
+                        if (desc) {
+                            if (_.isEmpty(desc.selectQuery))
+                                return;
+
+                            if (first)
+                                first = false;
+                            else
+                                sb.emptyLine();
+
+                            const cacheName = type.cache.name;
+
+                            if (!_.includes(cacheLoaded, cacheName)) {
+                                sb.append(`ignite.cache("${cacheName}").loadCache(null);`);
+
+                                sb.emptyLine();
+
+                                cacheLoaded.push(cacheName);
+                            }
+
+                            const varRows = rowVariableDeclared ? 'rows' : 'List<Cache.Entry<Object, Object>> rows';
+
+                            this._multilineQuery(sb, desc.selectQuery, `${varRows} = ignite.cache("${cacheName}").query(new SqlQuery<>("${getType(domain.valueType)}", `, ')).getAll();');
+
+                            sb.append('printResult(rows);');
+
+                            rowVariableDeclared = true;
+                        }
+                    });
+                });
+
+                sb.endBlock('}');
+            });
+
+            sb.endBlock('}');
+        }
+    }
+
+    /**
+     * Function to generate java class for node startup with cluster configuration.
+     *
+     * @param {Object} cluster Cluster to process.
+     * @param {String} fullClsName Full class name.
+     * @param {String} cfgRef Config.
+     * @param {String} [factoryCls] fully qualified class name of configuration factory.
+     * @param {Array.<Object>} [clientNearCaches] Is client node.
+     */
+    static nodeStartup(cluster, fullClsName, cfgRef, factoryCls, clientNearCaches) {
+        const dotIdx = fullClsName.lastIndexOf('.');
+
+        const pkg = fullClsName.substring(0, dotIdx);
+        const clsName = fullClsName.substring(dotIdx + 1);
+
+        const demo = clsName === 'DemoStartup';
+
+        const sb = new StringBuilder();
+
+        const imports = ['org.apache.ignite.Ignition'];
+
+        if (demo) {
+            imports.push('org.h2.tools.Server', 'java.sql.Connection', 'java.sql.PreparedStatement',
+                'java.sql.SQLException', 'java.util.Random', 'java.util.List', 'javax.cache.Cache',
+                'org.apache.ignite.cache.query.SqlQuery');
+        }
+
+        let shortFactoryCls;
+
+        if (factoryCls) {
+            imports.push(factoryCls);
+
+            shortFactoryCls = this.javaTypes.shortClassName(factoryCls);
+        }
+
+        if ((nonEmpty(clientNearCaches) || demo) && shortFactoryCls)
+            imports.push('org.apache.ignite.Ignite');
+
+        sb.append(`package ${pkg};`)
+            .emptyLine();
+
+        _.forEach(this._prepareImports(imports), (cls) => sb.append(`import ${cls};`));
+        sb.emptyLine();
+
+        if (demo) {
+            this.mainComment(sb,
+                'To start demo configure data sources in secret.properties file.',
+                'For H2 database it should be like following:',
+                'dsH2.jdbc.url=jdbc:h2:tcp://localhost/mem:DemoDB;DB_CLOSE_DELAY=-1',
+                'dsH2.jdbc.username=sa',
+                'dsH2.jdbc.password=',
+                ''
+            );
+        }
+        else
+            this.mainComment(sb);
+
+        sb.startBlock(`public class ${clsName} {`);
+
+        if (demo && shortFactoryCls)
+            this.demoStartup(sb, cluster, shortFactoryCls);
+
+        this.commentBlock(sb,
+            'Start up node with specified configuration.',
+            '',
+            '@param args Command line arguments, none required.',
+            '@throws Exception If failed.'
+        );
+        sb.startBlock('public static void main(String[] args) throws Exception {');
+
+        if (demo) {
+            sb.startBlock('try {');
+            sb.append('// Start H2 database server.');
+            sb.append('Server.createTcpServer("-tcpDaemon").start();');
+            sb.endBlock('}');
+            sb.startBlock('catch (SQLException ignore) {');
+            sb.append('// No-op.');
+            sb.endBlock('}');
+
+            sb.emptyLine();
+        }
+
+        if ((nonEmpty(clientNearCaches) || demo) && shortFactoryCls) {
+            imports.push('org.apache.ignite.Ignite');
+
+            sb.append(`Ignite ignite = Ignition.start(${cfgRef});`);
+
+            _.forEach(clientNearCaches, (cache, idx) => {
+                sb.emptyLine();
+
+                if (idx === 0)
+                    sb.append('// Demo of near cache creation on client node.');
+
+                const nearCacheMtd = this.javaTypes.toJavaName('nearConfiguration', cache.name);
+
+                sb.append(`ignite.getOrCreateCache(${shortFactoryCls}.${this.javaTypes.toJavaName('cache', cache.name)}(), ${shortFactoryCls}.${nearCacheMtd}());`);
+            });
+        }
+        else
+            sb.append(`Ignition.start(${cfgRef});`);
+
+        if (demo) {
+            sb.emptyLine();
+
+            sb.append('prepareDemoData();');
+
+            sb.emptyLine();
+
+            sb.append('runDemo(ignite);');
+        }
+
+        sb.endBlock('}');
+
+        sb.endBlock('}');
+
+        return sb.asString();
+    }
+
+    /**
+     * Function to generate java class for load caches.
+     *
+     * @param caches Caches to load.
+     * @param pkg Class package name.
+     * @param clsName Class name.
+     * @param {String} cfgRef Config.
+     */
+    static loadCaches(caches, pkg, clsName, cfgRef) {
+        const sb = new StringBuilder();
+
+        sb.append(`package ${pkg};`)
+            .emptyLine();
+
+        const imports = ['org.apache.ignite.Ignition', 'org.apache.ignite.Ignite'];
+
+        _.forEach(this._prepareImports(imports), (cls) => sb.append(`import ${cls};`));
+        sb.emptyLine();
+
+        this.mainComment(sb);
+        sb.startBlock(`public class ${clsName} {`);
+
+        this.commentBlock(sb,
+            '<p>',
+            'Utility to load caches from database.',
+            '<p>',
+            'How to use:',
+            '<ul>',
+            '    <li>Start cluster.</li>',
+            '    <li>Start this utility and wait while load complete.</li>',
+            '</ul>',
+            '',
+            '@param args Command line arguments, none required.',
+            '@throws Exception If failed.'
+        );
+        sb.startBlock('public static void main(String[] args) throws Exception {');
+
+        sb.startBlock(`try (Ignite ignite = Ignition.start(${cfgRef})) {`);
+
+        sb.append('System.out.println(">>> Loading caches...");');
+
+        sb.emptyLine();
+
+        _.forEach(caches, (cache) => {
+            sb.append('System.out.println(">>> Loading cache: ' + cache.name + '");');
+            sb.append('ignite.cache("' + cache.name + '").loadCache(null);');
+
+            sb.emptyLine();
+        });
+
+        sb.append('System.out.println(">>> All caches loaded!");');
+
+        sb.endBlock('}');
+
+        sb.endBlock('}');
+
+        sb.endBlock('}');
+
+        return sb.asString();
+    }
+
+    /**
+     * Checks if cluster has demo types.
+     *
+     * @param cluster Cluster to check.
+     * @param demo Is demo enabled.
+     * @returns {boolean} True if cluster has caches with demo types.
+     */
+    static isDemoConfigured(cluster, demo) {
+        return demo && _.find(cluster.caches, (cache) => _.find(cache.domains, (domain) => _.find(PREDEFINED_QUERIES, (desc) => domain.valueType.toUpperCase().endsWith(desc.type))));
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/Maven.service.js b/modules/frontend/app/configuration/generator/generator/Maven.service.js
new file mode 100644
index 0000000..fa99534
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/Maven.service.js
@@ -0,0 +1,266 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+import StringBuilder from './StringBuilder';
+import ArtifactVersionChecker from './ArtifactVersionChecker.service';
+import VersionService from 'app/services/Version.service';
+// Pom dependency information.
+import POM_DEPENDENCIES from 'app/data/pom-dependencies.json';
+
+const versionService = new VersionService();
+
+/**
+ * Pom file generation entry point.
+ */
+export default class IgniteMavenGenerator {
+    escapeId(s) {
+        if (typeof (s) !== 'string')
+            return s;
+
+        return s.replace(/[^A-Za-z0-9_\-.]+/g, '_');
+    }
+
+    addProperty(sb, tag, val) {
+        sb.append(`<${tag}>${val}</${tag}>`);
+    }
+
+    addComment(sb, comment) {
+        sb.append(`<!-- ${comment} -->`);
+    }
+
+    addDependency(deps, groupId, artifactId, version, jar, link) {
+        deps.push({groupId, artifactId, version, jar, link});
+    }
+
+    _extractVersion(igniteVer, version) {
+        return _.isArray(version) ? _.find(version, (v) => versionService.since(igniteVer, v.range)).version : version;
+    }
+
+    pickDependency(acc, key, dfltVer, igniteVer, storedVer) {
+        const deps = POM_DEPENDENCIES[key];
+
+        if (_.isNil(deps))
+            return;
+
+        _.forEach(_.castArray(deps), ({groupId, artifactId, version, jar, link}) => {
+            this.addDependency(acc, groupId || 'org.apache.ignite', artifactId, storedVer || this._extractVersion(igniteVer, version) || dfltVer, jar, link);
+        });
+    }
+
+    addResource(sb, dir, exclude) {
+        sb.startBlock('<resource>');
+
+        this.addProperty(sb, 'directory', dir);
+
+        if (exclude) {
+            sb.startBlock('<excludes>');
+            this.addProperty(sb, 'exclude', exclude);
+            sb.endBlock('</excludes>');
+        }
+
+        sb.endBlock('</resource>');
+    }
+
+    artifactSection(sb, cluster, targetVer) {
+        this.addProperty(sb, 'groupId', 'org.apache.ignite');
+        this.addProperty(sb, 'artifactId', this.escapeId(cluster.name) + '-project');
+        this.addProperty(sb, 'version', targetVer.ignite);
+    }
+
+    dependenciesSection(sb, deps) {
+        sb.startBlock('<dependencies>');
+
+        _.forEach(deps, (dep) => {
+            sb.startBlock('<dependency>');
+
+            this.addProperty(sb, 'groupId', dep.groupId);
+            this.addProperty(sb, 'artifactId', dep.artifactId);
+            this.addProperty(sb, 'version', dep.version);
+
+            if (dep.jar) {
+                this.addProperty(sb, 'scope', 'system');
+                this.addProperty(sb, 'systemPath', '${project.basedir}/jdbc-drivers/' + dep.jar);
+            }
+
+            if (dep.link)
+                this.addComment(sb, `You may download JDBC driver from: ${dep.link}`);
+
+            sb.endBlock('</dependency>');
+        });
+
+        sb.endBlock('</dependencies>');
+
+        return sb;
+    }
+
+    buildSection(sb = new StringBuilder(), excludeGroupIds) {
+        sb.startBlock('<build>');
+
+        sb.startBlock('<resources>');
+        this.addResource(sb, 'src/main/java', '**/*.java');
+        this.addResource(sb, 'src/main/resources');
+        sb.endBlock('</resources>');
+
+        sb.startBlock('<plugins>');
+
+        sb.startBlock('<plugin>');
+        this.addProperty(sb, 'artifactId', 'maven-dependency-plugin');
+        sb.startBlock('<executions>');
+        sb.startBlock('<execution>');
+        this.addProperty(sb, 'id', 'copy-libs');
+        this.addProperty(sb, 'phase', 'test-compile');
+        sb.startBlock('<goals>');
+        this.addProperty(sb, 'goal', 'copy-dependencies');
+        sb.endBlock('</goals>');
+        sb.startBlock('<configuration>');
+        this.addProperty(sb, 'excludeGroupIds', excludeGroupIds.join(','));
+        this.addProperty(sb, 'outputDirectory', 'target/libs');
+        this.addProperty(sb, 'includeScope', 'compile');
+        this.addProperty(sb, 'excludeTransitive', 'true');
+        sb.endBlock('</configuration>');
+        sb.endBlock('</execution>');
+        sb.endBlock('</executions>');
+        sb.endBlock('</plugin>');
+
+        sb.startBlock('<plugin>');
+        this.addProperty(sb, 'artifactId', 'maven-compiler-plugin');
+        this.addProperty(sb, 'version', '3.1');
+        sb.startBlock('<configuration>');
+        this.addProperty(sb, 'source', '1.7');
+        this.addProperty(sb, 'target', '1.7');
+        sb.endBlock('</configuration>');
+        sb.endBlock('</plugin>');
+
+        sb.endBlock('</plugins>');
+
+        sb.endBlock('</build>');
+    }
+
+    /**
+     * Add dependency for specified store factory if not exist.
+     *
+     * @param deps Already added dependencies.
+     * @param storeFactory Store factory to add dependency.
+     * @param igniteVer Ignite version.
+     */
+    storeFactoryDependency(deps, storeFactory, igniteVer) {
+        if (storeFactory.dialect && (!storeFactory.connectVia || storeFactory.connectVia === 'DataSource'))
+            this.pickDependency(deps, storeFactory.dialect, null, igniteVer, storeFactory.implementationVersion);
+    }
+
+    collectDependencies(cluster, targetVer) {
+        const igniteVer = targetVer.ignite;
+
+        const deps = [];
+        const storeDeps = [];
+
+        this.addDependency(deps, 'org.apache.ignite', 'ignite-core', igniteVer);
+
+        this.addDependency(deps, 'org.apache.ignite', 'ignite-spring', igniteVer);
+        this.addDependency(deps, 'org.apache.ignite', 'ignite-indexing', igniteVer);
+        this.addDependency(deps, 'org.apache.ignite', 'ignite-rest-http', igniteVer);
+
+        if (_.get(cluster, 'deploymentSpi.kind') === 'URI')
+            this.addDependency(deps, 'org.apache.ignite', 'ignite-urideploy', igniteVer);
+
+        this.pickDependency(deps, cluster.discovery.kind, igniteVer);
+
+        const caches = cluster.caches;
+
+        const blobStoreFactory = {cacheStoreFactory: {kind: 'CacheHibernateBlobStoreFactory'}};
+
+        _.forEach(caches, (cache) => {
+            if (cache.cacheStoreFactory && cache.cacheStoreFactory.kind)
+                this.storeFactoryDependency(storeDeps, cache.cacheStoreFactory[cache.cacheStoreFactory.kind], igniteVer);
+
+            if (_.get(cache, 'nodeFilter.kind') === 'Exclude')
+                this.addDependency(deps, 'org.apache.ignite', 'ignite-extdata-p2p', igniteVer);
+
+            if (cache.diskPageCompression && versionService.since(igniteVer, '2.8.0'))
+                this.addDependency(deps, 'org.apache.ignite', 'ignite-compress', igniteVer);
+        });
+
+        if (cluster.discovery.kind === 'Jdbc') {
+            const store = cluster.discovery.Jdbc;
+
+            if (store.dataSourceBean && store.dialect)
+                this.storeFactoryDependency(storeDeps, cluster.discovery.Jdbc, igniteVer);
+        }
+
+        _.forEach(cluster.checkpointSpi, (spi) => {
+            if (spi.kind === 'S3')
+                this.pickDependency(deps, spi.kind, igniteVer);
+            else if (spi.kind === 'JDBC')
+                this.storeFactoryDependency(storeDeps, spi.JDBC, igniteVer);
+        });
+
+        if (_.get(cluster, 'hadoopConfiguration.mapReducePlanner.kind') === 'Weighted' ||
+            _.find(cluster.igfss, (igfs) => igfs.secondaryFileSystemEnabled))
+            this.addDependency(deps, 'org.apache.ignite', 'ignite-hadoop', igniteVer);
+
+        if (_.find(caches, blobStoreFactory))
+            this.addDependency(deps, 'org.apache.ignite', 'ignite-hibernate', igniteVer);
+
+        if (cluster.logger && cluster.logger.kind)
+            this.pickDependency(deps, cluster.logger.kind, igniteVer);
+
+        return _.uniqWith(deps.concat(ArtifactVersionChecker.latestVersions(storeDeps)), _.isEqual);
+    }
+
+    /**
+     * Generate pom.xml.
+     *
+     * @param {Object} cluster Cluster  to take info about dependencies.
+     * @param {Object} targetVer Target version for dependencies.
+     * @returns {String} Generated content.
+     */
+    generate(cluster, targetVer) {
+        const sb = new StringBuilder();
+
+        sb.append('<?xml version="1.0" encoding="UTF-8"?>');
+
+        sb.emptyLine();
+
+        sb.append(`<!-- ${sb.generatedBy()} -->`);
+
+        sb.emptyLine();
+
+        sb.startBlock('<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">');
+
+        sb.append('<modelVersion>4.0.0</modelVersion>');
+
+        sb.emptyLine();
+
+        this.artifactSection(sb, cluster, targetVer);
+
+        sb.emptyLine();
+
+        const deps = this.collectDependencies(cluster, targetVer);
+
+        this.dependenciesSection(sb, deps);
+
+        sb.emptyLine();
+
+        this.buildSection(sb, ['org.apache.ignite']);
+
+        sb.endBlock('</project>');
+
+        return sb.asString();
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/PlatformGenerator.js b/modules/frontend/app/configuration/generator/generator/PlatformGenerator.js
new file mode 100644
index 0000000..0cb38a7
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/PlatformGenerator.js
@@ -0,0 +1,531 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+import {nonEmpty} from 'app/utils/lodashMixins';
+import {Bean, EmptyBean} from './Beans';
+
+/**
+ * @param {import('app/services/JavaTypes.service').default} JavaTypes
+ * @param {import('./defaults/Cluster.service').default} clusterDflts
+ * @param {import('./defaults/Cache.service').default} cacheDflts
+ */
+export default function service(JavaTypes, clusterDflts, cacheDflts) {
+    class PlatformGenerator {
+        static igniteConfigurationBean(cluster) {
+            return new Bean('Apache.Ignite.Core.IgniteConfiguration', 'cfg', cluster, clusterDflts);
+        }
+
+        static cacheConfigurationBean(cache) {
+            return new Bean('Apache.Ignite.Core.Cache.Configuration.CacheConfiguration', 'ccfg', cache, cacheDflts);
+        }
+
+        /**
+         * Function to generate ignite configuration.
+         *
+         * @param {Object} cluster Cluster to process.
+         * @return {String} Generated ignite configuration.
+         */
+        static igniteConfiguration(cluster) {
+            const cfg = this.igniteConfigurationBean(cluster);
+
+            this.clusterAtomics(cluster.atomics, cfg);
+
+            return cfg;
+        }
+
+        // Generate general section.
+        static clusterGeneral(cluster, cfg = this.igniteConfigurationBean(cluster)) {
+            cfg.stringProperty('name', 'GridName')
+                .stringProperty('localHost', 'Localhost');
+
+            if (_.isNil(cluster.discovery))
+                return cfg;
+
+            const discovery = new Bean('Apache.Ignite.Core.Discovery.Tcp.TcpDiscoverySpi', 'discovery',
+                cluster.discovery, clusterDflts.discovery);
+
+            let ipFinder;
+
+            switch (discovery.valueOf('kind')) {
+                case 'Vm':
+                    ipFinder = new Bean('Apache.Ignite.Core.Discovery.Tcp.Static.TcpDiscoveryStaticIpFinder',
+                        'ipFinder', cluster.discovery.Vm, clusterDflts.discovery.Vm);
+
+                    ipFinder.collectionProperty('addrs', 'addresses', cluster.discovery.Vm.addresses, 'ICollection');
+
+                    break;
+                case 'Multicast':
+                    ipFinder = new Bean('Apache.Ignite.Core.Discovery.Tcp.Multicast.TcpDiscoveryMulticastIpFinder',
+                        'ipFinder', cluster.discovery.Multicast, clusterDflts.discovery.Multicast);
+
+                    ipFinder.stringProperty('MulticastGroup')
+                        .intProperty('multicastPort', 'MulticastPort')
+                        .intProperty('responseWaitTime', 'ResponseTimeout')
+                        .intProperty('addressRequestAttempts', 'AddressRequestAttempts')
+                        .stringProperty('localAddress', 'LocalAddress')
+                        .collectionProperty('addrs', 'Endpoints', cluster.discovery.Multicast.addresses, 'ICollection');
+
+                    break;
+                default:
+            }
+
+            if (ipFinder)
+                discovery.beanProperty('IpFinder', ipFinder);
+
+            cfg.beanProperty('DiscoverySpi', discovery);
+
+
+            return cfg;
+        }
+
+        static clusterAtomics(atomics, cfg = this.igniteConfigurationBean()) {
+            const acfg = new Bean('Apache.Ignite.Core.DataStructures.Configuration.AtomicConfiguration', 'atomicCfg',
+                atomics, clusterDflts.atomics);
+
+            acfg.enumProperty('cacheMode', 'CacheMode')
+                .intProperty('atomicSequenceReserveSize', 'AtomicSequenceReserveSize');
+
+            if (acfg.valueOf('cacheMode') === 'PARTITIONED')
+                acfg.intProperty('backups', 'Backups');
+
+            if (acfg.isEmpty())
+                return cfg;
+
+            cfg.beanProperty('AtomicConfiguration', acfg);
+
+            return cfg;
+        }
+
+        // Generate binary group.
+        static clusterBinary(binary, cfg = this.igniteConfigurationBean()) {
+            const binaryCfg = new Bean('Apache.Ignite.Core.Binary.BinaryConfiguration', 'binaryCfg',
+                binary, clusterDflts.binary);
+
+            binaryCfg.emptyBeanProperty('idMapper', 'DefaultIdMapper')
+                .emptyBeanProperty('nameMapper', 'DefaultNameMapper')
+                .emptyBeanProperty('serializer', 'DefaultSerializer');
+
+            // const typeCfgs = [];
+            //
+            // _.forEach(binary.typeConfigurations, (type) => {
+            //     const typeCfg = new MethodBean('Apache.Ignite.Core.Binary.BinaryTypeConfiguration',
+            //         JavaTypes.toJavaName('binaryType', type.typeName), type, clusterDflts.binary.typeConfigurations);
+            //
+            //     typeCfg.stringProperty('typeName', 'TypeName')
+            //         .emptyBeanProperty('idMapper', 'IdMapper')
+            //         .emptyBeanProperty('nameMapper', 'NameMapper')
+            //         .emptyBeanProperty('serializer', 'Serializer')
+            //         .intProperty('enum', 'IsEnum');
+            //
+            //     if (typeCfg.nonEmpty())
+            //         typeCfgs.push(typeCfg);
+            // });
+            //
+            // binaryCfg.collectionProperty('types', 'TypeConfigurations', typeCfgs, 'ICollection',
+            //     'Apache.Ignite.Core.Binary.BinaryTypeConfiguration');
+            //
+            // binaryCfg.boolProperty('compactFooter', 'CompactFooter');
+            //
+            // if (binaryCfg.isEmpty())
+            //     return cfg;
+            //
+            // cfg.beanProperty('binaryConfiguration', binaryCfg);
+
+            return cfg;
+        }
+
+        // Generate communication group.
+        static clusterCommunication(cluster, cfg = this.igniteConfigurationBean(cluster)) {
+            const commSpi = new Bean('Apache.Ignite.Core.Communication.Tcp.TcpCommunicationSpi', 'communicationSpi',
+                cluster.communication, clusterDflts.communication);
+
+            commSpi.emptyBeanProperty('listener')
+                .stringProperty('localAddress')
+                .intProperty('localPort')
+                .intProperty('localPortRange')
+                // .intProperty('sharedMemoryPort')
+                .intProperty('directBuffer')
+                .intProperty('directSendBuffer')
+                .intProperty('idleConnectionTimeout')
+                .intProperty('connectTimeout')
+                .intProperty('maxConnectTimeout')
+                .intProperty('reconnectCount')
+                .intProperty('socketSendBuffer')
+                .intProperty('socketReceiveBuffer')
+                .intProperty('messageQueueLimit')
+                .intProperty('slowClientQueueLimit')
+                .intProperty('tcpNoDelay')
+                .intProperty('ackSendThreshold')
+                .intProperty('unacknowledgedMessagesBufferSize')
+                // .intProperty('socketWriteTimeout')
+                .intProperty('selectorsCount');
+            // .emptyBeanProperty('addressResolver');
+
+            if (commSpi.nonEmpty())
+                cfg.beanProperty('CommunicationSpi', commSpi);
+
+            cfg.intProperty('networkTimeout', 'NetworkTimeout')
+                .intProperty('networkSendRetryDelay')
+                .intProperty('networkSendRetryCount');
+            // .intProperty('discoveryStartupDelay');
+
+            return cfg;
+        }
+
+        // Generate discovery group.
+        static clusterDiscovery(discovery, cfg = this.igniteConfigurationBean()) {
+            if (discovery) {
+                let discoveryCfg = cfg.findProperty('discovery');
+
+                if (_.isNil(discoveryCfg)) {
+                    discoveryCfg = new Bean('Apache.Ignite.Core.Discovery.Tcp.TcpDiscoverySpi', 'discovery',
+                        discovery, clusterDflts.discovery);
+                }
+
+                discoveryCfg.stringProperty('localAddress')
+                    .intProperty('localPort')
+                    .intProperty('localPortRange')
+                    .intProperty('socketTimeout')
+                    .intProperty('ackTimeout')
+                    .intProperty('maxAckTimeout')
+                    .intProperty('networkTimeout')
+                    .intProperty('joinTimeout')
+                    .intProperty('threadPriority')
+                    .intProperty('heartbeatFrequency')
+                    .intProperty('maxMissedHeartbeats')
+                    .intProperty('maxMissedClientHeartbeats')
+                    .intProperty('topHistorySize')
+                    .intProperty('reconnectCount')
+                    .intProperty('statisticsPrintFrequency')
+                    .intProperty('ipFinderCleanFrequency')
+                    .intProperty('forceServerMode')
+                    .intProperty('clientReconnectDisabled');
+
+                if (discoveryCfg.nonEmpty())
+                    cfg.beanProperty('discoverySpi', discoveryCfg);
+            }
+
+            return cfg;
+        }
+
+        // Generate events group.
+        static clusterEvents(cluster, cfg = this.igniteConfigurationBean(cluster)) {
+            if (nonEmpty(cluster.includeEventTypes))
+                cfg.eventTypes('events', 'includeEventTypes', cluster.includeEventTypes);
+
+            return cfg;
+        }
+
+        // Generate metrics group.
+        static clusterMetrics(cluster, cfg = this.igniteConfigurationBean(cluster)) {
+            cfg.intProperty('metricsExpireTime')
+                .intProperty('metricsHistorySize')
+                .intProperty('metricsLogFrequency')
+                .intProperty('metricsUpdateFrequency');
+
+            return cfg;
+        }
+
+        // Generate transactions group.
+        static clusterTransactions(transactionConfiguration, cfg = this.igniteConfigurationBean()) {
+            const bean = new Bean('Apache.Ignite.Core.Transactions.TransactionConfiguration', 'TransactionConfiguration',
+                transactionConfiguration, clusterDflts.transactionConfiguration);
+
+            bean.enumProperty('defaultTxConcurrency', 'DefaultTransactionConcurrency')
+                .enumProperty('defaultTxIsolation', 'DefaultTransactionIsolation')
+                .intProperty('defaultTxTimeout', 'DefaultTimeout')
+                .intProperty('pessimisticTxLogLinger', 'PessimisticTransactionLogLinger')
+                .intProperty('pessimisticTxLogSize', 'PessimisticTransactionLogSize');
+
+            if (bean.nonEmpty())
+                cfg.beanProperty('transactionConfiguration', bean);
+
+            return cfg;
+        }
+
+        // Generate user attributes group.
+        static clusterUserAttributes(cluster, cfg = this.igniteConfigurationBean(cluster)) {
+            cfg.mapProperty('attributes', 'attributes', 'UserAttributes');
+
+            return cfg;
+        }
+
+        static clusterCaches(cluster, caches, igfss, isSrvCfg, cfg = this.igniteConfigurationBean(cluster)) {
+            // const cfg = this.clusterGeneral(cluster, cfg);
+            //
+            // if (nonEmpty(caches)) {
+            //     const ccfgs = _.map(caches, (cache) => this.cacheConfiguration(cache));
+            //
+            //     cfg.collectionProperty('', '', ccfgs, );
+            // }
+
+            return this.clusterGeneral(cluster, cfg);
+        }
+
+        // Generate cache general group.
+        static cacheGeneral(cache, ccfg = this.cacheConfigurationBean(cache)) {
+            ccfg.stringProperty('name')
+                .enumProperty('cacheMode')
+                .enumProperty('atomicityMode');
+
+            if (ccfg.valueOf('cacheMode') === 'PARTITIONED' && ccfg.valueOf('backups')) {
+                ccfg.intProperty('backups')
+                    .intProperty('readFromBackup');
+            }
+
+            ccfg.intProperty('copyOnRead');
+
+            if (ccfg.valueOf('cacheMode') === 'PARTITIONED' && ccfg.valueOf('atomicityMode') === 'TRANSACTIONAL')
+                ccfg.intProperty('isInvalidate', 'invalidate');
+
+            return ccfg;
+        }
+
+        // Generate cache memory group.
+        static cacheMemory(cache, available, ccfg = this.cacheConfigurationBean(cache)) {
+            ccfg.enumProperty('memoryMode');
+
+            if (ccfg.valueOf('memoryMode') !== 'OFFHEAP_VALUES')
+                ccfg.intProperty('offHeapMaxMemory');
+
+            // this._evictionPolicy(ccfg, available, false, cache.evictionPolicy, cacheDflts.evictionPolicy);
+
+            ccfg.intProperty('startSize')
+                .boolProperty('swapEnabled', 'EnableSwap');
+
+            return ccfg;
+        }
+
+        // Generate cache queries & Indexing group.
+        static cacheQuery(cache, domains, ccfg = this.cacheConfigurationBean(cache)) {
+            ccfg.intProperty('sqlOnheapRowCacheSize')
+                .intProperty('longQueryWarningTimeout');
+
+            return ccfg;
+        }
+
+        // Generate cache store group.
+        static cacheStore(cache, domains, ccfg = this.cacheConfigurationBean(cache)) {
+            const kind = _.get(cache, 'cacheStoreFactory.kind');
+
+            if (kind && cache.cacheStoreFactory[kind]) {
+                let bean = null;
+
+                const storeFactory = cache.cacheStoreFactory[kind];
+
+                switch (kind) {
+                    case 'CacheJdbcPojoStoreFactory':
+                        bean = new Bean('org.apache.ignite.cache.store.jdbc.CacheJdbcPojoStoreFactory', 'cacheStoreFactory',
+                            storeFactory);
+
+                        const id = bean.valueOf('dataSourceBean');
+
+                        bean.dataSource(id, 'dataSourceBean', this.dataSourceBean(id, storeFactory.dialect))
+                            .beanProperty('dialect', new EmptyBean(this.dialectClsName(storeFactory.dialect)));
+
+                        const setType = (typeBean, propName) => {
+                            if (JavaTypes.nonBuiltInClass(typeBean.valueOf(propName)))
+                                typeBean.stringProperty(propName);
+                            else
+                                typeBean.classProperty(propName);
+                        };
+
+                        const types = _.reduce(domains, (acc, domain) => {
+                            if (_.isNil(domain.databaseTable))
+                                return acc;
+
+                            const typeBean = new Bean('org.apache.ignite.cache.store.jdbc.JdbcType', 'type',
+                                _.merge({}, domain, {cacheName: cache.name}))
+                                .stringProperty('cacheName');
+
+                            setType(typeBean, 'keyType');
+                            setType(typeBean, 'valueType');
+
+                            this.domainStore(domain, typeBean);
+
+                            acc.push(typeBean);
+
+                            return acc;
+                        }, []);
+
+                        bean.arrayProperty('types', 'types', types, 'org.apache.ignite.cache.store.jdbc.JdbcType');
+
+                        break;
+                    case 'CacheJdbcBlobStoreFactory':
+                        bean = new Bean('org.apache.ignite.cache.store.jdbc.CacheJdbcBlobStoreFactory', 'cacheStoreFactory',
+                            storeFactory);
+
+                        if (bean.valueOf('connectVia') === 'DataSource')
+                            bean.dataSource(bean.valueOf('dataSourceBean'), 'dataSourceBean', this.dialectClsName(storeFactory.dialect));
+                        else {
+                            ccfg.stringProperty('connectionUrl')
+                                .stringProperty('user')
+                                .property('password', `ds.${storeFactory.user}.password`, 'YOUR_PASSWORD');
+                        }
+
+                        bean.boolProperty('initSchema')
+                            .stringProperty('createTableQuery')
+                            .stringProperty('loadQuery')
+                            .stringProperty('insertQuery')
+                            .stringProperty('updateQuery')
+                            .stringProperty('deleteQuery');
+
+                        break;
+                    case 'CacheHibernateBlobStoreFactory':
+                        bean = new Bean('org.apache.ignite.cache.store.hibernate.CacheHibernateBlobStoreFactory',
+                            'cacheStoreFactory', storeFactory);
+
+                        bean.propsProperty('props', 'hibernateProperties');
+
+                        break;
+                    default:
+                }
+
+                if (bean)
+                    ccfg.beanProperty('cacheStoreFactory', bean);
+            }
+
+            ccfg.boolProperty('storeKeepBinary')
+                .boolProperty('loadPreviousValue')
+                .boolProperty('readThrough')
+                .boolProperty('writeThrough');
+
+            if (ccfg.valueOf('writeBehindEnabled')) {
+                ccfg.boolProperty('writeBehindEnabled')
+                    .intProperty('writeBehindBatchSize')
+                    .intProperty('writeBehindFlushSize')
+                    .intProperty('writeBehindFlushFrequency')
+                    .intProperty('writeBehindFlushThreadCount');
+            }
+
+            return ccfg;
+        }
+
+        // Generate cache concurrency control group.
+        static cacheConcurrency(cache, ccfg = this.cacheConfigurationBean(cache)) {
+            ccfg.intProperty('maxConcurrentAsyncOperations')
+                .intProperty('defaultLockTimeout')
+                .enumProperty('atomicWriteOrderMode')
+                .enumProperty('writeSynchronizationMode');
+
+            return ccfg;
+        }
+
+        // Generate cache node filter group.
+        static cacheNodeFilter(cache, igfss, ccfg = this.cacheConfigurationBean(cache)) {
+            const kind = _.get(cache, 'nodeFilter.kind');
+
+            if (kind && cache.nodeFilter[kind]) {
+                let bean = null;
+
+                switch (kind) {
+                    case 'IGFS':
+                        const foundIgfs = _.find(igfss, (igfs) => igfs._id === cache.nodeFilter.IGFS.igfs);
+
+                        if (foundIgfs) {
+                            bean = new Bean('org.apache.ignite.internal.processors.igfs.IgfsNodePredicate', 'nodeFilter', foundIgfs)
+                                .stringConstructorArgument('name');
+                        }
+
+                        break;
+                    case 'Custom':
+                        bean = new Bean(cache.nodeFilter.Custom.className, 'nodeFilter');
+
+                        break;
+                    default:
+                        return ccfg;
+                }
+
+                if (bean)
+                    ccfg.beanProperty('nodeFilter', bean);
+            }
+
+            return ccfg;
+        }
+
+        // Generate cache rebalance group.
+        static cacheRebalance(cache, ccfg = this.cacheConfigurationBean(cache)) {
+            if (ccfg.valueOf('cacheMode') !== 'LOCAL') {
+                ccfg.enumProperty('rebalanceMode')
+                    .intProperty('rebalanceThreadPoolSize')
+                    .intProperty('rebalanceBatchSize')
+                    .intProperty('rebalanceBatchesPrefetchCount')
+                    .intProperty('rebalanceOrder')
+                    .intProperty('rebalanceDelay')
+                    .intProperty('rebalanceTimeout')
+                    .intProperty('rebalanceThrottle');
+            }
+
+            if (ccfg.includes('igfsAffinnityGroupSize')) {
+                const bean = new Bean('org.apache.ignite.igfs.IgfsGroupDataBlocksKeyMapper', 'affinityMapper', cache)
+                    .intConstructorArgument('igfsAffinnityGroupSize');
+
+                ccfg.beanProperty('affinityMapper', bean);
+            }
+
+            return ccfg;
+        }
+
+        // Generate server near cache group.
+        static cacheServerNearCache(cache, ccfg = this.cacheConfigurationBean(cache)) {
+            if (cache.cacheMode === 'PARTITIONED' && cache.nearCacheEnabled) {
+                const bean = new Bean('org.apache.ignite.configuration.NearCacheConfiguration', 'nearConfiguration',
+                    cache.nearConfiguration, {nearStartSize: 375000});
+
+                bean.intProperty('nearStartSize');
+
+                this._evictionPolicy(bean, true,
+                    bean.valueOf('nearEvictionPolicy'), cacheDflts.evictionPolicy);
+
+                ccfg.beanProperty('nearConfiguration', bean);
+            }
+
+            return ccfg;
+        }
+
+        // Generate cache statistics group.
+        static cacheStatistics(cache, ccfg = this.cacheConfigurationBean(cache)) {
+            ccfg.boolProperty('statisticsEnabled')
+                .boolProperty('managementEnabled');
+
+            return ccfg;
+        }
+
+        static cacheConfiguration(cache, ccfg = this.cacheConfigurationBean(cache)) {
+            this.cacheGeneral(cache, ccfg);
+            this.cacheMemory(cache, ccfg);
+            this.cacheQuery(cache, cache.domains, ccfg);
+            this.cacheStore(cache, cache.domains, ccfg);
+
+            const igfs = _.get(cache, 'nodeFilter.IGFS.instance');
+            this.cacheNodeFilter(cache, igfs ? [igfs] : [], ccfg);
+            this.cacheConcurrency(cache, ccfg);
+            this.cacheRebalance(cache, ccfg);
+            this.cacheServerNearCache(cache, ccfg);
+            this.cacheStatistics(cache, ccfg);
+            // this.cacheDomains(cache.domains, cfg);
+
+            return ccfg;
+        }
+    }
+
+    return PlatformGenerator;
+}
+
+service.$inject = ['JavaTypes', 'igniteClusterPlatformDefaults', 'igniteCachePlatformDefaults'];
diff --git a/modules/frontend/app/configuration/generator/generator/Properties.service.js b/modules/frontend/app/configuration/generator/generator/Properties.service.js
new file mode 100644
index 0000000..882ac5b
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/Properties.service.js
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import StringBuilder from './StringBuilder';
+
+/**
+ * Properties generation entry point.
+ */
+export default class IgnitePropertiesGenerator {
+    _collectProperties(bean) {
+        const props = [];
+
+        // Append properties for complex object.
+        const processBean = (bean) => {
+            const newProps = _.difference(this._collectProperties(bean), props);
+
+            if (!_.isEmpty(newProps)) {
+                props.push(...newProps);
+
+                if (!_.isEmpty(_.last(props)))
+                    props.push('');
+            }
+        };
+
+        // Append properties from item.
+        const processItem = (item) => {
+            switch (item.clsName) {
+                case 'PROPERTY':
+                case 'PROPERTY_CHAR':
+                case 'PROPERTY_INT':
+                    props.push(..._.difference([`${item.value}=${item.hint}`], props));
+
+                    break;
+                case 'BEAN':
+                case 'DATA_SOURCE':
+                    processBean(item.value);
+
+                    break;
+                case 'ARRAY':
+                case 'COLLECTION':
+                    _.forEach(item.items, processBean);
+
+                    break;
+                case 'MAP':
+                    // Generate properties for all objects in keys and values of map.
+                    _.forEach(item.entries, (entry) => {
+                        processBean(entry.name);
+                        processBean(entry.value);
+                    });
+
+                    break;
+                default:
+                    // No-op.
+            }
+        };
+
+        // Generate properties for object arguments.
+        _.forEach(_.get(bean, 'arguments'), processItem);
+
+        // Generate properties for object properties.
+        _.forEach(_.get(bean, 'properties'), processItem);
+
+        return props;
+    }
+
+    generate(cfg) {
+        const lines = this._collectProperties(cfg);
+
+        if (_.isEmpty(lines))
+            return null;
+
+        const sb = new StringBuilder();
+
+        sb.append(`# ${sb.generatedBy()}`).emptyLine();
+
+        _.forEach(lines, (line) => sb.append(line));
+
+        return sb.asString();
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/Readme.service.js b/modules/frontend/app/configuration/generator/generator/Readme.service.js
new file mode 100644
index 0000000..d070912
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/Readme.service.js
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import StringBuilder from './StringBuilder';
+
+/**
+ * Properties generation entry point.
+ */
+export default class IgniteReadmeGenerator {
+    header(sb) {
+        sb.append('Content of this folder was generated by Apache Ignite Web Console');
+        sb.append('=================================================================');
+    }
+
+    /**
+     * Generate README.txt for jdbc folder.
+     *
+     * @param sb Resulting output with generated readme.
+     * @returns {string} Generated content.
+     */
+    generateJDBC(sb = new StringBuilder()) {
+        sb.append('Proprietary JDBC drivers for databases like Oracle, IBM DB2, Microsoft SQL Server are not available on Maven Central repository.');
+        sb.append('Drivers should be downloaded manually and copied to this folder.');
+
+        return sb.asString();
+    }
+
+    /**
+     * Generate README.txt.
+     *
+     * @returns {string} Generated content.
+     */
+    generate(sb = new StringBuilder()) {
+        this.header(sb);
+        sb.emptyLine();
+
+        sb.append('Project structure:');
+        sb.append('    /jdbc-drivers - this folder should contains proprietary JDBC drivers.');
+        sb.append('    /src - this folder contains generated java code.');
+        sb.append('    /src/main/java/config - this folder contains generated java classes with cluster configuration from code.');
+        sb.append('    /src/main/java/startup - this folder contains generated java classes with server and client nodes startup code.');
+        sb.append('    /src/main/java/[model] - this optional folder will be named as package name for your POJO classes and contain generated POJO files.');
+        sb.append('    /src/main/resources - this folder contains generated configurations in XML format and secret.properties file with security sensitive information if any.');
+        sb.append('    Dockerfile - sample Docker file. With this file you could package Ignite deployment with all the dependencies into a standard container.');
+        sb.append('    pom.xml - generated Maven project description, could be used to open generated project in IDE or build with Maven.');
+        sb.append('    README.txt - this file.');
+
+        sb.emptyLine();
+
+        sb.append('Ignite ships with CacheJdbcPojoStore, which is out-of-the-box JDBC implementation of the IgniteCacheStore ');
+        sb.append('interface, and automatically handles all the write-through and read-through logic.');
+
+        sb.emptyLine();
+
+        sb.append('You can use generated configuration and POJO classes as part of your application.');
+
+        sb.emptyLine();
+
+        sb.append('Note, in case of using proprietary JDBC drivers (Oracle, IBM DB2, Microsoft SQL Server)');
+        sb.append('you should download them manually and copy into ./jdbc-drivers folder.');
+
+        return sb.asString();
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/SharpTransformer.service.js b/modules/frontend/app/configuration/generator/generator/SharpTransformer.service.js
new file mode 100644
index 0000000..e7d5d7b
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/SharpTransformer.service.js
@@ -0,0 +1,258 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+import AbstractTransformer from './AbstractTransformer';
+import StringBuilder from './StringBuilder';
+
+import ConfigurationGenerator from './ConfigurationGenerator';
+
+import ClusterDefaults from './defaults/Cluster.service';
+import CacheDefaults from './defaults/Cache.service';
+import IGFSDefaults from './defaults/IGFS.service';
+
+import JavaTypes from '../../../services/JavaTypes.service';
+import {JavaTypesNonEnum} from '../JavaTypesNonEnum.service';
+
+const generator = new ConfigurationGenerator();
+
+const clusterDflts = new ClusterDefaults();
+const cacheDflts = new CacheDefaults();
+const igfsDflts = new IGFSDefaults();
+
+const javaTypes = new JavaTypes();
+const javaTypesNonEnum = new JavaTypesNonEnum(clusterDflts, cacheDflts, igfsDflts, javaTypes);
+
+export default class SharpTransformer extends AbstractTransformer {
+    static generator = generator;
+
+    static commentBlock(sb, ...lines) {
+        _.forEach(lines, (line) => sb.append(`// ${line}`));
+    }
+
+    static doc(sb, ...lines) {
+        sb.append('/// <summary>');
+        _.forEach(lines, (line) => sb.append(`/// ${line}`));
+        sb.append('/// </summary>');
+    }
+
+    static mainComment(sb) {
+        return this.doc(sb, sb.generatedBy());
+    }
+
+    /**
+     *
+     * @param {Array.<String>} sb
+     * @param {Bean} bean
+     */
+    static _defineBean(sb, bean) {
+        const shortClsName = javaTypes.shortClassName(bean.clsName);
+
+        sb.append(`var ${bean.id} = new ${shortClsName}();`);
+    }
+
+    /**
+     * @param {StringBuilder} sb
+     * @param {Bean} parent
+     * @param {Bean} propertyName
+     * @param {String|Bean} value
+     * @private
+     */
+    static _setProperty(sb, parent, propertyName, value) {
+        sb.append(`${parent.id}.${_.upperFirst(propertyName)} = ${value};`);
+    }
+
+    /**
+     *
+     * @param {StringBuilder} sb
+     * @param {Bean} parent
+     * @param {String} propertyName
+     * @param {Bean} bean
+     * @private
+     */
+    static _setBeanProperty(sb, parent, propertyName, bean) {
+        sb.append(`${parent.id}.${_.upperFirst(propertyName)} = ${bean.id};`);
+    }
+
+    static _toObject(clsName, val) {
+        const items = _.isArray(val) ? val : [val];
+
+        return _.map(items, (item, idx) => {
+            if (_.isNil(item))
+                return 'null';
+
+            const shortClsName = javaTypes.shortClassName(clsName);
+
+            switch (shortClsName) {
+                // case 'byte':
+                //     return `(byte) ${item}`;
+                // case 'Serializable':
+                case 'String':
+                    if (items.length > 1)
+                        return `"${item}"${idx !== items.length - 1 ? ' +' : ''}`;
+
+                    return `"${item}"`;
+                // case 'Path':
+                //     return `"${item.replace(/\\/g, '\\\\')}"`;
+                // case 'Class':
+                //     return `${this.shortClassName(item)}.class`;
+                // case 'UUID':
+                //     return `UUID.fromString("${item}")`;
+                // case 'PropertyChar':
+                //     return `props.getProperty("${item}").toCharArray()`;
+                // case 'Property':
+                //     return `props.getProperty("${item}")`;
+                // case 'Bean':
+                //     if (item.isComplex())
+                //         return item.id;
+                //
+                //     return this._newBean(item);
+                default:
+                    if (javaTypesNonEnum.nonEnum(shortClsName))
+                        return item;
+
+                    return `${shortClsName}.${item}`;
+            }
+        });
+    }
+
+    /**
+     *
+     * @param {StringBuilder} sb
+     * @param {Bean} bean
+     * @returns {Array}
+     */
+    static _setProperties(sb = new StringBuilder(), bean) {
+        _.forEach(bean.properties, (prop) => {
+            switch (prop.clsName) {
+                case 'ICollection':
+                    // const implClsName = JavaTypes.shortClassName(prop.implClsName);
+
+                    const colTypeClsName = javaTypes.shortClassName(prop.typeClsName);
+
+                    if (colTypeClsName === 'String') {
+                        const items = this._toObject(colTypeClsName, prop.items);
+
+                        sb.append(`${bean.id}.${_.upperFirst(prop.name)} = new {${items.join(', ')}};`);
+                    }
+                    // else {
+                    //     if (_.includes(vars, prop.id))
+                    //         sb.append(`${prop.id} = new ${implClsName}<>();`);
+                    //     else {
+                    //         vars.push(prop.id);
+                    //
+                    //         sb.append(`${clsName}<${colTypeClsName}> ${prop.id} = new ${implClsName}<>();`);
+                    //     }
+                    //
+                    //     sb.emptyLine();
+                    //
+                    //     if (nonBean) {
+                    //         const items = this._toObject(colTypeClsName, prop.items);
+                    //
+                    //         _.forEach(items, (item) => {
+                    //             sb.append(`${prop.id}.add("${item}");`);
+                    //
+                    //             sb.emptyLine();
+                    //         });
+                    //     }
+                    //     else {
+                    //         _.forEach(prop.items, (item) => {
+                    //             this.constructBean(sb, item, vars, limitLines);
+                    //
+                    //             sb.append(`${prop.id}.add(${item.id});`);
+                    //
+                    //             sb.emptyLine();
+                    //         });
+                    //
+                    //         this._setProperty(sb, bean.id, prop.name, prop.id);
+                    //     }
+                    // }
+
+                    break;
+
+                case 'Bean':
+                    const nestedBean = prop.value;
+
+                    this._defineBean(sb, nestedBean);
+
+                    sb.emptyLine();
+
+                    this._setProperties(sb, nestedBean);
+
+                    sb.emptyLine();
+
+                    this._setBeanProperty(sb, bean, prop.name, nestedBean);
+
+                    break;
+                default:
+                    this._setProperty(sb, bean, prop.name, this._toObject(prop.clsName, prop.value));
+            }
+        });
+
+        return sb;
+    }
+
+    /**
+     * Build Java startup class with configuration.
+     *
+     * @param {Bean} cfg
+     * @param pkg Package name.
+     * @param clsName Class name for generate factory class otherwise generate code snippet.
+     * @returns {String}
+     */
+    static toClassFile(cfg, pkg, clsName) {
+        const sb = new StringBuilder();
+
+        sb.startBlock(`namespace ${pkg}`, '{');
+
+        _.forEach(_.sortBy(cfg.collectClasses()), (cls) => sb.append(`using ${cls};`));
+        sb.emptyLine();
+
+
+        this.mainComment(sb);
+        sb.startBlock(`public class ${clsName}`, '{');
+
+        this.doc(sb, 'Configure grid.');
+        sb.startBlock('public static IgniteConfiguration CreateConfiguration()', '{');
+
+        this._defineBean(sb, cfg);
+
+        sb.emptyLine();
+
+        this._setProperties(sb, cfg);
+
+        sb.emptyLine();
+
+        sb.append(`return ${cfg.id};`);
+
+        sb.endBlock('}');
+
+        sb.endBlock('}');
+
+        sb.endBlock('}');
+
+        return sb.asString();
+    }
+
+    static generateSection(bean) {
+        const sb = new StringBuilder();
+
+        this._setProperties(sb, bean);
+
+        return sb.asString();
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/SpringTransformer.service.js b/modules/frontend/app/configuration/generator/generator/SpringTransformer.service.js
new file mode 100644
index 0000000..6ce50d4
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/SpringTransformer.service.js
@@ -0,0 +1,346 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+import {Bean} from './Beans';
+
+import AbstractTransformer from './AbstractTransformer';
+import StringBuilder from './StringBuilder';
+import VersionService from 'app/services/Version.service';
+
+const versionService = new VersionService();
+
+export default class IgniteSpringTransformer extends AbstractTransformer {
+    static escapeXml(str = '') {
+        return str.replace(/&/g, '&amp;')
+            .replace(/"/g, '&quot;')
+            .replace(/'/g, '&apos;')
+            .replace(/>/g, '&gt;')
+            .replace(/</g, '&lt;');
+    }
+
+    static commentBlock(sb, ...lines) {
+        if (lines.length > 1) {
+            sb.append('<!--');
+
+            _.forEach(lines, (line) => sb.append(`  ${line}`));
+
+            sb.append('-->');
+        }
+        else
+            sb.append(`<!-- ${_.head(lines)} -->`);
+    }
+
+    static appendBean(sb, bean, appendId) {
+        const beanTags = [];
+
+        if (appendId)
+            beanTags.push(`id="${bean.id}"`);
+
+        beanTags.push(`class="${bean.clsName}"`);
+
+        if (bean.factoryMtd)
+            beanTags.push(`factory-method="${bean.factoryMtd}"`);
+
+        sb.startBlock(`<bean ${beanTags.join(' ')}>`);
+
+        _.forEach(bean.arguments, (arg) => {
+            if (arg.clsName === 'MAP') {
+                sb.startBlock('<constructor-arg>');
+                this._constructMap(sb, arg);
+                sb.endBlock('</constructor-arg>');
+            }
+            else if (_.isNil(arg.value)) {
+                sb.startBlock('<constructor-arg>');
+                sb.append('<null/>');
+                sb.endBlock('</constructor-arg>');
+            }
+            else if (arg.constant) {
+                sb.startBlock('<constructor-arg>');
+                sb.append(`<util:constant static-field="${arg.clsName}.${arg.value}"/>`);
+                sb.endBlock('</constructor-arg>');
+            }
+            else if (arg.clsName === 'BEAN') {
+                sb.startBlock('<constructor-arg>');
+                this.appendBean(sb, arg.value);
+                sb.endBlock('</constructor-arg>');
+            }
+            else
+                sb.append(`<constructor-arg value="${this._toObject(arg.clsName, arg.value)}"/>`);
+        });
+
+        this._setProperties(sb, bean);
+
+        sb.endBlock('</bean>');
+    }
+
+    static _toObject(clsName, val) {
+        const items = _.isArray(val) ? val : [val];
+
+        if (clsName === 'EVENTS')
+            return ['<list>', ..._.map(items, (item) => `    <util:constant static-field="${item.class}.${item.label}"/>`), '</list>'];
+
+        return _.map(items, (item) => {
+            switch (clsName) {
+                case 'PROPERTY':
+                case 'PROPERTY_CHAR':
+                case 'PROPERTY_INT':
+                    return `\${${item}}`;
+                case 'java.lang.Class':
+                    return this.javaTypes.fullClassName(item);
+                case 'long':
+                    return `${item}`;
+                case 'java.lang.String':
+                case 'PATH':
+                case 'PATH_ARRAY':
+                    return this.escapeXml(item);
+                default:
+                    return item;
+            }
+        });
+    }
+
+    static _isBean(clsName) {
+        return this.javaTypes.nonBuiltInClass(clsName) && this.javaTypesNonEnum.nonEnum(clsName) && _.includes(clsName, '.');
+    }
+
+    static _setCollection(sb, prop) {
+        sb.startBlock(`<property name="${prop.name}">`);
+        sb.startBlock('<list>');
+
+        _.forEach(prop.items, (item, idx) => {
+            if (this._isBean(prop.typeClsName)) {
+                if (idx !== 0)
+                    sb.emptyLine();
+
+                this.appendBean(sb, item);
+            }
+            else
+                sb.append(`<value>${item}</value>`);
+        });
+
+        sb.endBlock('</list>');
+        sb.endBlock('</property>');
+    }
+
+    static _constructMap(sb, map) {
+        sb.startBlock('<map>');
+
+        _.forEach(map.entries, (entry) => {
+            const key = entry[map.keyField];
+            const val = entry[map.valField];
+
+            const isKeyBean = key instanceof Bean || this._isBean(map.keyClsName);
+            const isValBean = val instanceof Bean || this._isBean(map.valClsName);
+
+
+            if (isKeyBean || isValBean) {
+                sb.startBlock('<entry>');
+
+                sb.startBlock('<key>');
+                if (isKeyBean)
+                    this.appendBean(sb, key);
+                else
+                    sb.append(this._toObject(map.keyClsName, key));
+                sb.endBlock('</key>');
+
+                if (!_.isArray(val))
+                    sb.startBlock('<value>');
+
+                if (isValBean)
+                    this.appendBean(sb, val);
+                else
+                    sb.append(this._toObject(map.valClsNameShow || map.valClsName, val));
+
+                if (!_.isArray(val))
+                    sb.endBlock('</value>');
+
+                sb.endBlock('</entry>');
+            }
+            else
+                sb.append(`<entry key="${this._toObject(map.keyClsName, key)}" value="${this._toObject(map.valClsName, val)}"/>`);
+        });
+
+        sb.endBlock('</map>');
+    }
+
+    /**
+     *
+     * @param {StringBuilder} sb
+     * @param {Bean} bean
+     * @returns {StringBuilder}
+     */
+    static _setProperties(sb, bean) {
+        _.forEach(bean.properties, (prop, idx) => {
+            switch (prop.clsName) {
+                case 'DATA_SOURCE':
+                    const valAttr = prop.name === 'dataSource' ? 'ref' : 'value';
+
+                    sb.append(`<property name="${prop.name}" ${valAttr}="${prop.id}"/>`);
+
+                    break;
+                case 'EVENT_TYPES':
+                    sb.startBlock(`<property name="${prop.name}">`);
+
+                    if (prop.eventTypes.length === 1) {
+                        const evtGrp = _.head(prop.eventTypes);
+
+                        sb.append(`<util:constant static-field="${evtGrp.class}.${evtGrp.label}"/>`);
+                    }
+                    else {
+                        sb.startBlock('<list>');
+
+                        _.forEach(prop.eventTypes, (evtGrp, ix) => {
+                            ix > 0 && sb.emptyLine();
+
+                            sb.append(`<!-- EventType.${evtGrp.label} -->`);
+
+                            _.forEach(evtGrp.events, (event) =>
+                                sb.append(`<util:constant static-field="${evtGrp.class}.${event}"/>`));
+                        });
+
+                        sb.endBlock('</list>');
+                    }
+
+                    sb.endBlock('</property>');
+
+                    break;
+                case 'ARRAY':
+                case 'PATH_ARRAY':
+                case 'COLLECTION':
+                    this._setCollection(sb, prop);
+
+                    break;
+                case 'MAP':
+                    sb.startBlock(`<property name="${prop.name}">`);
+
+                    this._constructMap(sb, prop);
+
+                    sb.endBlock('</property>');
+
+                    break;
+                case 'java.util.Properties':
+                    sb.startBlock(`<property name="${prop.name}">`);
+                    sb.startBlock('<props>');
+
+                    _.forEach(prop.entries, (entry) => {
+                        sb.append(`<prop key="${entry.name}">${entry.value}</prop>`);
+                    });
+
+                    sb.endBlock('</props>');
+                    sb.endBlock('</property>');
+
+                    break;
+                case 'BEAN':
+                    sb.startBlock(`<property name="${prop.name}">`);
+
+                    this.appendBean(sb, prop.value);
+
+                    sb.endBlock('</property>');
+
+                    break;
+                default:
+                    sb.append(`<property name="${prop.name}" value="${this._toObject(prop.clsName, prop.value)}"/>`);
+            }
+
+            this._emptyLineIfNeeded(sb, bean.properties, idx);
+        });
+
+        return sb;
+    }
+
+    /**
+     * Build final XML.
+     *
+     * @param {Bean} cfg Ignite configuration.
+     * @param {Object} targetVer Version of Ignite for generated project.
+     * @param {Boolean} clientNearCaches
+     * @returns {StringBuilder}
+     */
+    static igniteConfiguration(cfg, targetVer, clientNearCaches) {
+        const available = versionService.since.bind(versionService, targetVer.ignite);
+
+        const sb = new StringBuilder();
+
+        // 0. Add header.
+        sb.append('<?xml version="1.0" encoding="UTF-8"?>');
+        sb.emptyLine();
+
+        this.mainComment(sb);
+        sb.emptyLine();
+
+        // 1. Start beans section.
+        sb.startBlock([
+            '<beans xmlns="http://www.springframework.org/schema/beans"',
+            '       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
+            '       xmlns:util="http://www.springframework.org/schema/util"',
+            '       xsi:schemaLocation="http://www.springframework.org/schema/beans',
+            '                           http://www.springframework.org/schema/beans/spring-beans.xsd',
+            '                           http://www.springframework.org/schema/util',
+            '                           http://www.springframework.org/schema/util/spring-util.xsd">']);
+
+        // 2. Add external property file
+        if (this.hasProperties(cfg)) {
+            this.commentBlock(sb, 'Load external properties file.');
+
+            sb.startBlock('<bean id="placeholderConfig" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">');
+            sb.append('<property name="location" value="classpath:secret.properties"/>');
+            sb.endBlock('</bean>');
+
+            sb.emptyLine();
+        }
+
+        // 3. Add data sources.
+        const dataSources = this.collectDataSources(cfg);
+
+        if (dataSources.length) {
+            this.commentBlock(sb, 'Data source beans will be initialized from external properties file.');
+
+            _.forEach(dataSources, (ds) => {
+                this.appendBean(sb, ds, true);
+
+                sb.emptyLine();
+            });
+        }
+
+        _.forEach(clientNearCaches, (cache) => {
+            this.commentBlock(sb, `Configuration of near cache for cache "${cache.name}"`);
+
+            this.appendBean(sb, this.generator.cacheNearClient(cache, available), true);
+
+            sb.emptyLine();
+        });
+
+        // 3. Add main content.
+        this.appendBean(sb, cfg);
+
+        // 4. Close beans section.
+        sb.endBlock('</beans>');
+
+        return sb;
+    }
+
+    static cluster(cluster, targetVer, client) {
+        const cfg = this.generator.igniteConfiguration(cluster, targetVer, client);
+
+        const clientNearCaches = client ? _.filter(cluster.caches, (cache) =>
+            cache.cacheMode === 'PARTITIONED' && _.get(cache, 'clientNearConfiguration.enabled')) : [];
+
+        return this.igniteConfiguration(cfg, targetVer, clientNearCaches);
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/StringBuilder.js b/modules/frontend/app/configuration/generator/generator/StringBuilder.js
new file mode 100644
index 0000000..338bf1e
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/StringBuilder.js
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const DATE_OPTS = {
+    month: '2-digit',
+    day: '2-digit',
+    year: 'numeric',
+    hour: '2-digit',
+    minute: '2-digit',
+    hour12: false
+};
+
+export default class StringBuilder {
+    generatedBy() {
+        return `This file was generated by Ignite Web Console (${new Date().toLocaleString('en-US', DATE_OPTS)})`;
+    }
+
+    /**
+     * @param deep
+     * @param indent
+     */
+    constructor(deep = 0, indent = 4) {
+        this.indent = indent;
+        this.deep = deep;
+        this.lines = [];
+    }
+
+    emptyLine() {
+        this.lines.push('');
+
+        return this;
+    }
+
+    append(lines) {
+        if (_.isArray(lines))
+            _.forEach(lines, (line) => this.lines.push(_.repeat(' ', this.indent * this.deep) + line));
+        else
+            this.lines.push(_.repeat(' ', this.indent * this.deep) + lines);
+
+        return this;
+    }
+
+    startBlock(lines) {
+        this.append(lines);
+
+        this.deep++;
+
+        return this;
+    }
+
+    endBlock(line) {
+        this.deep--;
+
+        this.append(line);
+
+        return this;
+    }
+
+    asString() {
+        return this.lines.join('\n');
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/defaults/Cache.platform.service.js b/modules/frontend/app/configuration/generator/generator/defaults/Cache.platform.service.js
new file mode 100644
index 0000000..eeac3a0
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/defaults/Cache.platform.service.js
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+const enumValueMapper = (val) => _.capitalize(val);
+
+const DFLT_CACHE = {
+    cacheMode: {
+        clsName: 'Apache.Ignite.Core.Cache.Configuration.CacheMode',
+        mapper: enumValueMapper
+    },
+    atomicityMode: {
+        clsName: 'Apache.Ignite.Core.Cache.Configuration.CacheAtomicityMode',
+        mapper: enumValueMapper
+    },
+    memoryMode: {
+        clsName: 'Apache.Ignite.Core.Cache.Configuration.CacheMemoryMode',
+        value: 'ONHEAP_TIERED',
+        mapper: enumValueMapper
+    },
+    atomicWriteOrderMode: {
+        clsName: 'org.apache.ignite.cache.CacheAtomicWriteOrderMode',
+        mapper: enumValueMapper
+    },
+    writeSynchronizationMode: {
+        clsName: 'org.apache.ignite.cache.CacheWriteSynchronizationMode',
+        value: 'PRIMARY_SYNC',
+        mapper: enumValueMapper
+    },
+    rebalanceMode: {
+        clsName: 'org.apache.ignite.cache.CacheRebalanceMode',
+        value: 'ASYNC',
+        mapper: enumValueMapper
+    }
+};
+
+export default class IgniteCachePlatformDefaults {
+    constructor() {
+        Object.assign(this, DFLT_CACHE);
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/defaults/Cache.service.js b/modules/frontend/app/configuration/generator/generator/defaults/Cache.service.js
new file mode 100644
index 0000000..94fc8db
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/defaults/Cache.service.js
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const DFLT_CACHE = {
+    cacheMode: {
+        clsName: 'org.apache.ignite.cache.CacheMode'
+    },
+    partitionLossPolicy: {
+        clsName: 'org.apache.ignite.cache.PartitionLossPolicy',
+        value: 'IGNORE'
+    },
+    atomicityMode: {
+        clsName: 'org.apache.ignite.cache.CacheAtomicityMode'
+    },
+    memoryMode: {
+        clsName: 'org.apache.ignite.cache.CacheMemoryMode',
+        value: 'ONHEAP_TIERED'
+    },
+    onheapCacheEnabled: false,
+    offHeapMaxMemory: -1,
+    startSize: 1500000,
+    swapEnabled: false,
+    sqlOnheapRowCacheSize: 10240,
+    longQueryWarningTimeout: 3000,
+    snapshotableIndex: false,
+    sqlEscapeAll: false,
+    storeKeepBinary: false,
+    loadPreviousValue: false,
+    cacheStoreFactory: {
+        CacheJdbcPojoStoreFactory: {
+            batchSize: 512,
+            maximumWriteAttempts: 2,
+            parallelLoadCacheMinimumThreshold: 512,
+            sqlEscapeAll: false
+        }
+    },
+    storeConcurrentLoadAllThreshold: 5,
+    readThrough: false,
+    writeThrough: false,
+    writeBehindEnabled: false,
+    writeBehindBatchSize: 512,
+    writeBehindFlushSize: 10240,
+    writeBehindFlushFrequency: 5000,
+    writeBehindFlushThreadCount: 1,
+    writeBehindCoalescing: true,
+    maxConcurrentAsyncOperations: 500,
+    defaultLockTimeout: 0,
+    atomicWriteOrderMode: {
+        clsName: 'org.apache.ignite.cache.CacheAtomicWriteOrderMode'
+    },
+    writeSynchronizationMode: {
+        clsName: 'org.apache.ignite.cache.CacheWriteSynchronizationMode',
+        value: 'PRIMARY_SYNC'
+    },
+    rebalanceMode: {
+        clsName: 'org.apache.ignite.cache.CacheRebalanceMode',
+        value: 'ASYNC'
+    },
+    rebalanceBatchSize: 524288,
+    rebalanceBatchesPrefetchCount: 2,
+    rebalanceOrder: 0,
+    rebalanceDelay: 0,
+    rebalanceTimeout: 10000,
+    rebalanceThrottle: 0,
+    statisticsEnabled: false,
+    managementEnabled: false,
+    nearConfiguration: {
+        nearStartSize: 375000
+    },
+    clientNearConfiguration: {
+        nearStartSize: 375000
+    },
+    evictionPolicy: {
+        batchSize: 1,
+        maxSize: 100000
+    },
+    queryMetadata: 'Configuration',
+    queryDetailMetricsSize: 0,
+    queryParallelism: 1,
+    fields: {
+        keyClsName: 'java.lang.String',
+        valClsName: 'java.lang.String',
+        valField: 'className',
+        entries: []
+    },
+    defaultFieldValues: {
+        keyClsName: 'java.lang.String',
+        valClsName: 'java.lang.Object'
+    },
+    fieldsPrecision: {
+        keyClsName: 'java.lang.String',
+        valClsName: 'java.lang.Integer'
+    },
+    fieldsScale: {
+        keyClsName: 'java.lang.String',
+        valClsName: 'java.lang.Integer'
+    },
+    aliases: {
+        keyClsName: 'java.lang.String',
+        valClsName: 'java.lang.String',
+        keyField: 'field',
+        valField: 'alias',
+        entries: []
+    },
+    indexes: {
+        indexType: {
+            clsName: 'org.apache.ignite.cache.QueryIndexType'
+        },
+        fields: {
+            keyClsName: 'java.lang.String',
+            valClsName: 'java.lang.Boolean',
+            valField: 'direction',
+            entries: []
+        }
+    },
+    typeField: {
+        databaseFieldType: {
+            clsName: 'java.sql.Types'
+        }
+    },
+    memoryPolicyName: 'default',
+    diskPageCompression: {
+        clsName: 'org.apache.ignite.configuration.DiskPageCompression'
+    },
+    sqlOnheapCacheEnabled: false,
+    sqlOnheapCacheMaxSize: 0,
+    storeByValue: false,
+    encryptionEnabled: false,
+    eventsDisabled: false,
+    maxQueryIteratorsCount: 1024
+};
+
+export default class IgniteCacheDefaults {
+    constructor() {
+        Object.assign(this, DFLT_CACHE);
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/defaults/Cluster.platform.service.js b/modules/frontend/app/configuration/generator/generator/defaults/Cluster.platform.service.js
new file mode 100644
index 0000000..b701951
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/defaults/Cluster.platform.service.js
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const enumValueMapper = (val) => _.capitalize(val);
+
+const DFLT_CLUSTER = {
+    atomics: {
+        cacheMode: {
+            clsName: 'Apache.Ignite.Core.Cache.Configuration.CacheMode',
+            mapper: enumValueMapper
+        }
+    },
+    transactionConfiguration: {
+        defaultTxConcurrency: {
+            clsName: 'Apache.Ignite.Core.Transactions.TransactionConcurrency',
+            mapper: enumValueMapper
+        },
+        defaultTxIsolation: {
+            clsName: 'Apache.Ignite.Core.Transactions.TransactionIsolation',
+            mapper: enumValueMapper
+        }
+    }
+};
+
+export default class IgniteClusterPlatformDefaults {
+    constructor() {
+        Object.assign(this, DFLT_CLUSTER);
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/defaults/Cluster.service.js b/modules/frontend/app/configuration/generator/generator/defaults/Cluster.service.js
new file mode 100644
index 0000000..76e9671
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/defaults/Cluster.service.js
@@ -0,0 +1,475 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const DFLT_CLUSTER = {
+    localHost: '0.0.0.0',
+    activeOnStart: true,
+    cacheSanityCheckEnabled: true,
+    discovery: {
+        localPort: 47500,
+        localPortRange: 100,
+        socketTimeout: 5000,
+        ackTimeout: 5000,
+        maxAckTimeout: 600000,
+        networkTimeout: 5000,
+        joinTimeout: 0,
+        threadPriority: 10,
+        heartbeatFrequency: 2000,
+        maxMissedHeartbeats: 1,
+        maxMissedClientHeartbeats: 5,
+        topHistorySize: 1000,
+        reconnectCount: 10,
+        statisticsPrintFrequency: 0,
+        ipFinderCleanFrequency: 60000,
+        forceServerMode: false,
+        clientReconnectDisabled: false,
+        reconnectDelay: 2000,
+        connectionRecoveryTimeout: 10000,
+        soLinger: 5,
+        Multicast: {
+            multicastGroup: '228.1.2.4',
+            multicastPort: 47400,
+            responseWaitTime: 500,
+            addressRequestAttempts: 2,
+            localAddress: '0.0.0.0'
+        },
+        Jdbc: {
+            initSchema: false
+        },
+        SharedFs: {
+            path: 'disco/tcp'
+        },
+        ZooKeeper: {
+            basePath: '/services',
+            serviceName: 'ignite',
+            allowDuplicateRegistrations: false,
+            ExponentialBackoff: {
+                baseSleepTimeMs: 1000,
+                maxRetries: 10
+            },
+            BoundedExponentialBackoffRetry: {
+                baseSleepTimeMs: 1000,
+                maxSleepTimeMs: 2147483647,
+                maxRetries: 10
+            },
+            UntilElapsed: {
+                maxElapsedTimeMs: 60000,
+                sleepMsBetweenRetries: 1000
+            },
+            RetryNTimes: {
+                n: 10,
+                sleepMsBetweenRetries: 1000
+            },
+            OneTime: {
+                sleepMsBetweenRetry: 1000
+            },
+            Forever: {
+                retryIntervalMs: 1000
+            }
+        },
+        Kubernetes: {
+            serviceName: 'ignite',
+            namespace: 'default',
+            masterUrl: 'https://kubernetes.default.svc.cluster.local:443',
+            accountToken: '/var/run/secrets/kubernetes.io/serviceaccount/token'
+        }
+    },
+    atomics: {
+        atomicSequenceReserveSize: 1000,
+        cacheMode: {
+            clsName: 'org.apache.ignite.cache.CacheMode',
+            value: 'PARTITIONED'
+        }
+    },
+    binary: {
+        compactFooter: true,
+        typeConfigurations: {
+            enum: false,
+            enumValues: {
+                keyClsName: 'java.lang.String',
+                valClsName: 'java.lang.Integer',
+                entries: []
+            }
+        }
+    },
+    collision: {
+        kind: null,
+        JobStealing: {
+            activeJobsThreshold: 95,
+            waitJobsThreshold: 0,
+            messageExpireTime: 1000,
+            maximumStealingAttempts: 5,
+            stealingEnabled: true,
+            stealingAttributes: {
+                keyClsName: 'java.lang.String',
+                valClsName: 'java.io.Serializable',
+                items: []
+            }
+        },
+        PriorityQueue: {
+            priorityAttributeKey: 'grid.task.priority',
+            jobPriorityAttributeKey: 'grid.job.priority',
+            defaultPriority: 0,
+            starvationIncrement: 1,
+            starvationPreventionEnabled: true
+        }
+    },
+    communication: {
+        localPort: 47100,
+        localPortRange: 100,
+        sharedMemoryPort: 48100,
+        directBuffer: false,
+        directSendBuffer: false,
+        idleConnectionTimeout: 30000,
+        connectTimeout: 5000,
+        maxConnectTimeout: 600000,
+        reconnectCount: 10,
+        socketSendBuffer: 32768,
+        socketReceiveBuffer: 32768,
+        messageQueueLimit: 1024,
+        tcpNoDelay: true,
+        ackSendThreshold: 16,
+        unacknowledgedMessagesBufferSize: 0,
+        socketWriteTimeout: 2000,
+        selectorSpins: 0,
+        connectionsPerNode: 1,
+        usePairedConnections: false,
+        filterReachableAddresses: false
+    },
+    networkTimeout: 5000,
+    networkSendRetryDelay: 1000,
+    networkSendRetryCount: 3,
+    discoveryStartupDelay: 60000,
+    connector: {
+        port: 11211,
+        portRange: 100,
+        idleTimeout: 7000,
+        idleQueryCursorTimeout: 600000,
+        idleQueryCursorCheckFrequency: 60000,
+        receiveBufferSize: 32768,
+        sendBufferSize: 32768,
+        sendQueueLimit: 0,
+        directBuffer: false,
+        noDelay: true,
+        sslEnabled: false,
+        sslClientAuth: false
+    },
+    deploymentMode: {
+        clsName: 'org.apache.ignite.configuration.DeploymentMode',
+        value: 'SHARED'
+    },
+    peerClassLoadingEnabled: false,
+    peerClassLoadingMissedResourcesCacheSize: 100,
+    peerClassLoadingThreadPoolSize: 2,
+    failoverSpi: {
+        JobStealing: {
+            maximumFailoverAttempts: 5
+        },
+        Always: {
+            maximumFailoverAttempts: 5
+        }
+    },
+    failureDetectionTimeout: 10000,
+    clientFailureDetectionTimeout: 30000,
+    logger: {
+        Log4j: {
+            level: {
+                clsName: 'org.apache.log4j.Level'
+            }
+        },
+        Log4j2: {
+            level: {
+                clsName: 'org.apache.logging.log4j.Level'
+            }
+        }
+    },
+    marshalLocalJobs: false,
+    marshallerCacheKeepAliveTime: 10000,
+    metricsHistorySize: 10000,
+    metricsLogFrequency: 60000,
+    metricsUpdateFrequency: 2000,
+    clockSyncSamples: 8,
+    clockSyncFrequency: 120000,
+    timeServerPortBase: 31100,
+    timeServerPortRange: 100,
+    transactionConfiguration: {
+        defaultTxConcurrency: {
+            clsName: 'org.apache.ignite.transactions.TransactionConcurrency',
+            value: 'PESSIMISTIC'
+        },
+        defaultTxIsolation: {
+            clsName: 'org.apache.ignite.transactions.TransactionIsolation',
+            value: 'REPEATABLE_READ'
+        },
+        defaultTxTimeout: 0,
+        pessimisticTxLogLinger: 10000,
+        useJtaSynchronization: false,
+        txTimeoutOnPartitionMapExchange: 0,
+        deadlockTimeout: 10000
+    },
+    attributes: {
+        keyClsName: 'java.lang.String',
+        valClsName: 'java.lang.String',
+        items: []
+    },
+    odbcConfiguration: {
+        endpointAddress: '0.0.0.0:10800..10810',
+        socketSendBufferSize: 0,
+        socketReceiveBufferSize: 0,
+        maxOpenCursors: 128
+    },
+    eventStorage: {
+        Memory: {
+            expireCount: 10000
+        }
+    },
+    checkpointSpi: {
+        S3: {
+            bucketNameSuffix: 'default-bucket',
+            clientConfiguration: {
+                protocol: {
+                    clsName: 'com.amazonaws.Protocol',
+                    value: 'HTTPS'
+                },
+                maxConnections: 50,
+                retryPolicy: {
+                    retryCondition: {
+                        clsName: 'com.amazonaws.retry.PredefinedRetryPolicies'
+                    },
+                    backoffStrategy: {
+                        clsName: 'com.amazonaws.retry.PredefinedRetryPolicies'
+                    },
+                    maxErrorRetry: {
+                        clsName: 'com.amazonaws.retry.PredefinedRetryPolicies'
+                    },
+                    honorMaxErrorRetryInClientConfig: false
+                },
+                maxErrorRetry: -1,
+                socketTimeout: 50000,
+                connectionTimeout: 50000,
+                requestTimeout: 0,
+                socketSendBufferSizeHints: 0,
+                connectionTTL: -1,
+                connectionMaxIdleMillis: 60000,
+                responseMetadataCacheSize: 50,
+                useReaper: true,
+                useGzip: false,
+                preemptiveBasicProxyAuth: false,
+                useTcpKeepAlive: false,
+                cacheResponseMetadata: true,
+                clientExecutionTimeout: 0,
+                socketSendBufferSizeHint: 0,
+                socketReceiveBufferSizeHint: 0,
+                useExpectContinue: true,
+                useThrottleRetries: true
+            }
+        },
+        JDBC: {
+            checkpointTableName: 'CHECKPOINTS',
+            keyFieldName: 'NAME',
+            keyFieldType: 'VARCHAR',
+            valueFieldName: 'VALUE',
+            valueFieldType: 'BLOB',
+            expireDateFieldName: 'EXPIRE_DATE',
+            expireDateFieldType: 'DATETIME',
+            numberOfRetries: 2
+        }
+    },
+    loadBalancingSpi: {
+        RoundRobin: {
+            perTask: false
+        },
+        Adaptive: {
+            loadProbe: {
+                Job: {
+                    useAverage: true
+                },
+                CPU: {
+                    useAverage: true,
+                    useProcessors: true,
+                    processorCoefficient: 1
+                },
+                ProcessingTime: {
+                    useAverage: true
+                }
+            }
+        },
+        WeightedRandom: {
+            nodeWeight: 10,
+            useWeights: false
+        }
+    },
+    memoryConfiguration: {
+        systemCacheInitialSize: 41943040,
+        systemCacheMaxSize: 104857600,
+        pageSize: 2048,
+        defaultMemoryPolicyName: 'default',
+        memoryPolicies: {
+            name: 'default',
+            initialSize: 268435456,
+            pageEvictionMode: {
+                clsName: 'org.apache.ignite.configuration.DataPageEvictionMode',
+                value: 'DISABLED'
+            },
+            evictionThreshold: 0.9,
+            emptyPagesPoolSize: 100,
+            metricsEnabled: false,
+            subIntervals: 5,
+            rateTimeInterval: 60000
+        }
+    },
+    dataStorageConfiguration: {
+        systemCacheInitialSize: 41943040,
+        systemCacheMaxSize: 104857600,
+        pageSize: 4096,
+        storagePath: 'db',
+        dataRegionConfigurations: {
+            name: 'default',
+            initialSize: 268435456,
+            pageEvictionMode: {
+                clsName: 'org.apache.ignite.configuration.DataPageEvictionMode',
+                value: 'DISABLED'
+            },
+            evictionThreshold: 0.9,
+            emptyPagesPoolSize: 100,
+            metricsEnabled: false,
+            metricsSubIntervalCount: 5,
+            metricsRateTimeInterval: 60000,
+            checkpointPageBufferSize: 0
+        },
+        metricsEnabled: false,
+        alwaysWriteFullPages: false,
+        checkpointFrequency: 180000,
+        checkpointPageBufferSize: 268435456,
+        checkpointThreads: 4,
+        checkpointWriteOrder: {
+            clsName: 'org.apache.ignite.configuration.CheckpointWriteOrder',
+            value: 'SEQUENTIAL'
+        },
+        walMode: {
+            clsName: 'org.apache.ignite.configuration.WALMode',
+            value: 'DEFAULT'
+        },
+        walPath: 'db/wal',
+        walArchivePath: 'db/wal/archive',
+        walSegments: 10,
+        walSegmentSize: 67108864,
+        walHistorySize: 20,
+        walFlushFrequency: 2000,
+        walFsyncDelayNanos: 1000,
+        walRecordIteratorBufferSize: 67108864,
+        lockWaitTime: 10000,
+        walThreadLocalBufferSize: 131072,
+        metricsSubIntervalCount: 5,
+        metricsRateTimeInterval: 60000,
+        maxWalArchiveSize: 1073741824,
+        walCompactionLevel: 1
+    },
+    utilityCacheKeepAliveTime: 60000,
+    hadoopConfiguration: {
+        mapReducePlanner: {
+            Weighted: {
+                localMapperWeight: 100,
+                remoteMapperWeight: 100,
+                localReducerWeight: 100,
+                remoteReducerWeight: 100,
+                preferLocalReducerThresholdWeight: 200
+            }
+        },
+        finishedJobInfoTtl: 30000,
+        maxTaskQueueSize: 8192
+    },
+    serviceConfigurations: {
+        maxPerNodeCount: 0,
+        totalCount: 0
+    },
+    longQueryWarningTimeout: 3000,
+    persistenceStoreConfiguration: {
+        metricsEnabled: false,
+        alwaysWriteFullPages: false,
+        checkpointingFrequency: 180000,
+        checkpointingPageBufferSize: 268435456,
+        checkpointingThreads: 1,
+        walSegments: 10,
+        walSegmentSize: 67108864,
+        walHistorySize: 20,
+        walFlushFrequency: 2000,
+        walFsyncDelayNanos: 1000,
+        walRecordIteratorBufferSize: 67108864,
+        lockWaitTime: 10000,
+        rateTimeInterval: 60000,
+        tlbSize: 131072,
+        subIntervals: 5,
+        walMode: {
+            clsName: 'org.apache.ignite.configuration.WALMode',
+            value: 'DEFAULT'
+        },
+        walAutoArchiveAfterInactivity: -1
+    },
+    sqlConnectorConfiguration: {
+        port: 10800,
+        portRange: 100,
+        socketSendBufferSize: 0,
+        socketReceiveBufferSize: 0,
+        tcpNoDelay: true,
+        maxOpenCursorsPerConnection: 128
+    },
+    clientConnectorConfiguration: {
+        port: 10800,
+        portRange: 100,
+        socketSendBufferSize: 0,
+        socketReceiveBufferSize: 0,
+        tcpNoDelay: true,
+        maxOpenCursorsPerConnection: 128,
+        idleTimeout: 0,
+        jdbcEnabled: true,
+        odbcEnabled: true,
+        thinClientEnabled: true,
+        sslEnabled: false,
+        useIgniteSslContextFactory: true,
+        sslClientAuth: false
+    },
+    encryptionSpi: {
+        Keystore: {
+            keySize: 256,
+            masterKeyName: 'ignite.master.key'
+        }
+    },
+    failureHandler: {
+        ignoredFailureTypes: {clsName: 'org.apache.ignite.failure.FailureType'}
+    },
+    localEventListeners: {
+        keyClsName: 'org.apache.ignite.lang.IgnitePredicate',
+        keyClsGenericType: 'org.apache.ignite.events.Event',
+        isKeyClsGenericTypeExtended: true,
+        valClsName: 'int[]',
+        valClsNameShow: 'EVENTS',
+        keyField: 'className',
+        valField: 'eventTypes'
+    },
+    authenticationEnabled: false,
+    sqlQueryHistorySize: 1000,
+    allSegmentationResolversPassRequired: true,
+    networkCompressionLevel: 1,
+    autoActivationEnabled: true
+};
+
+export default class IgniteClusterDefaults {
+    constructor() {
+        Object.assign(this, DFLT_CLUSTER);
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/defaults/Event-groups.service.js b/modules/frontend/app/configuration/generator/generator/defaults/Event-groups.service.js
new file mode 100644
index 0000000..e5a11ed
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/defaults/Event-groups.service.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+// Events groups.
+import EVENT_GROUPS from 'app/data/event-groups.json';
+
+export default class IgniteEventGroups {
+    constructor() {
+        return _.clone(EVENT_GROUPS);
+    }
+}
diff --git a/modules/frontend/app/configuration/generator/generator/defaults/IGFS.service.js b/modules/frontend/app/configuration/generator/generator/defaults/IGFS.service.js
new file mode 100644
index 0000000..056742a
--- /dev/null
+++ b/modules/frontend/app/configuration/generator/generator/defaults/IGFS.service.js
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const DFLT_IGFS = {
+    defaultMode: {
+        clsName: 'org.apache.ignite.igfs.IgfsMode',
+        value: 'DUAL_ASYNC'
+    },
+    secondaryFileSystem: {
+        userNameMapper: {
+            Basic: {
+                mappings: {
+                    keyClsName: 'java.lang.String',
+                    valClsName: 'java.lang.String',
+                    keyField: 'name',
+                    valField: 'value',
+                    entries: []
+                }
+            }
+        },
+        Kerberos: {
+            reloginInterval: 600000
+        }
+    },
+    ipcEndpointConfiguration: {
+        type: {
+            clsName: 'org.apache.ignite.igfs.IgfsIpcEndpointType'
+        },
+        host: '127.0.0.1',
+        port: 10500,
+        memorySize: 262144,
+        tokenDirectoryPath: 'ipc/shmem'
+    },
+    fragmentizerConcurrentFiles: 0,
+    fragmentizerThrottlingBlockLength: 16777216,
+    fragmentizerThrottlingDelay: 200,
+    dualModeMaxPendingPutsSize: 0,
+    dualModePutExecutorServiceShutdown: false,
+    blockSize: 65536,
+    streamBufferSize: 65536,
+    maxSpaceSize: 0,
+    maximumTaskRangeLength: 0,
+    managementPort: 11400,
+    perNodeBatchSize: 100,
+    perNodeParallelBatchCount: 8,
+    prefetchBlocks: 0,
+    sequentialReadsBeforePrefetch: 0,
+    trashPurgeTimeout: 1000,
+    colocateMetadata: true,
+    relaxedConsistency: true,
+    pathModes: {
+        keyClsName: 'java.lang.String',
+        keyField: 'path',
+        valClsName: 'org.apache.ignite.igfs.IgfsMode',
+        valField: 'mode'
+    },
+    updateFileLengthOnFlush: false
+};
+
+export default class IgniteIGFSDefaults {
+    constructor() {
+        Object.assign(this, DFLT_IGFS);
+    }
+}
diff --git a/modules/frontend/app/configuration/icons/configuration.icon.svg b/modules/frontend/app/configuration/icons/configuration.icon.svg
new file mode 100644
index 0000000..7c0ac7a
--- /dev/null
+++ b/modules/frontend/app/configuration/icons/configuration.icon.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" viewBox="0 0 20 17">
+    <g fill="none" fill-rule="evenodd" transform="translate(0 1)">
+        <rect width="11" height="2" x="9" y="1" fill="currentColor" rx="1"/>
+        <rect width="5" height="2" x="15" y="7" fill="currentColor" rx="1"/>
+        <rect width="8" height="2" x="12" y="13" fill="currentColor" rx="1"/>
+        <rect width="5" height="2" y="1" fill="currentColor" rx="1"/>
+        <rect width="11" height="2" y="7" fill="currentColor" rx="1"/>
+        <rect width="8" height="2" y="13" fill="currentColor" rx="1"/>
+        <circle cx="5" cy="1.776" r="1" stroke="currentColor" stroke-width="2"/>
+        <circle cx="11" cy="7.776" r="1" stroke="currentColor" stroke-width="2"/>
+        <circle cx="8" cy="13.776" r="1" stroke="currentColor" stroke-width="2"/>
+    </g>
+</svg>
diff --git a/modules/frontend/app/configuration/index.lazy.ts b/modules/frontend/app/configuration/index.lazy.ts
new file mode 100644
index 0000000..d4d87c9
--- /dev/null
+++ b/modules/frontend/app/configuration/index.lazy.ts
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {LazyLoadResult, UIRouter} from '@uirouter/angularjs';
+import {default as configurationIcon} from './icons/configuration.icon.svg';
+import {default as IconsService} from '../components/ignite-icon/service';
+import {AppStore, navigationMenuItem} from '../store';
+
+export default angular
+    .module('ignite-console.configuration-lazy', [])
+    .run(['$uiRouter', '$injector', function($uiRouter: UIRouter, $injector: ng.auto.IInjectorService) {
+        $uiRouter.stateRegistry.register({
+            name: 'base.configuration.**',
+            url: '/configuration',
+            async lazyLoad($transition$) {
+                const module = await import(/* webpackChunkName: "configuration" */'./index');
+                $injector.loadNewModules([module.default.name]);
+                return [] as LazyLoadResult;
+            }
+        });
+    }])
+    .run(['IgniteIcon', (icons: IconsService) => { icons.registerIcons({configuration: configurationIcon}); }])
+    .run(['Store', (store: AppStore) => {
+        store.dispatch(navigationMenuItem({
+            activeSref: 'base.configuration.**',
+            icon: 'configuration',
+            label: 'Configuration',
+            order: 1,
+            sref: 'base.configuration.overview'
+        }));
+    }])
+    .config(['DefaultStateProvider', (DefaultState) => {
+        DefaultState.setRedirectTo(() => 'base.configuration.overview');
+    }]);
diff --git a/modules/frontend/app/configuration/index.ts b/modules/frontend/app/configuration/index.ts
new file mode 100644
index 0000000..abaf013
--- /dev/null
+++ b/modules/frontend/app/configuration/index.ts
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import uiValidate from 'angular-ui-validate';
+import {UIRouterRx} from '@uirouter/rx';
+import {UIRouter} from '@uirouter/angularjs';
+
+import {filter, scan, tap, withLatestFrom} from 'rxjs/operators';
+
+import generatorModule from './generator/configuration.module';
+
+import ConfigureState from './services/ConfigureState';
+import PageConfigure from './services/PageConfigure';
+import ConfigurationDownload from './services/ConfigurationDownload';
+import ConfigChangesGuard from './services/ConfigChangesGuard';
+import ConfigSelectionManager from './services/ConfigSelectionManager';
+import SummaryZipper from './services/SummaryZipper';
+import ConfigurationResource from './services/ConfigurationResource';
+import selectors from './store/selectors';
+import effects from './store/effects';
+import Clusters from './services/Clusters';
+import Caches from './services/Caches';
+import IGFSs from './services/IGFSs';
+import Models from './services/Models';
+
+import pageConfigure from './components/page-configure';
+import pageConfigureBasic from './components/page-configure-basic';
+import pageConfigureAdvanced from './components/page-configure-advanced';
+import pageConfigureOverview from './components/page-configure-overview';
+
+import projectStructurePreview from './components/modal-preview-project';
+import itemsTable from './components/pc-items-table';
+import pcUiGridFilters from './components/pc-ui-grid-filters';
+import isInCollection from './components/pcIsInCollection';
+import pcValidation from './components/pcValidation';
+import fakeUiCanExit from './components/fakeUICanExit';
+import formUICanExitGuard from './components/formUICanExitGuard';
+import modalImportModels from './components/modal-import-models';
+import buttonImportModels from './components/button-import-models';
+import buttonDownloadProject from './components/button-download-project';
+import buttonPreviewProject from './components/button-preview-project';
+import previewPanel from './components/preview-panel';
+import pcSplitButton from './components/pc-split-button';
+import uiAceTabs from './components/ui-ace-tabs.directive';
+
+import uiAceJava from './components/ui-ace-java';
+import uiAceSpring from './components/ui-ace-spring';
+
+import {registerStates} from './states';
+
+import {
+    basicCachesActionTypes,
+    cachesActionTypes,
+    clustersActionTypes,
+    editReducer,
+    editReducer2,
+    igfssActionTypes,
+    itemsEditReducerFactory,
+    loadingReducer,
+    mapCacheReducerFactory,
+    mapStoreReducerFactory,
+    modelsActionTypes,
+    refsReducer,
+    shortCachesActionTypes,
+    shortClustersActionTypes,
+    shortIGFSsActionTypes,
+    shortModelsActionTypes,
+    shortObjectsReducer
+} from './store/reducer';
+
+import {errorState} from './transitionHooks/errorState';
+import {default as ActivitiesData} from '../core/activities/Activities.data';
+
+const JDBC_LINKS = {
+    Oracle: 'https://www.oracle.com/technetwork/database/application-development/jdbc/downloads/index.html',
+    DB2: 'http://www-01.ibm.com/support/docview.wss?uid=swg21363866'
+};
+
+registerActivitiesHook.$inject = ['$uiRouter', 'IgniteActivitiesData'];
+
+function registerActivitiesHook($uiRouter: UIRouter, ActivitiesData: ActivitiesData) {
+    $uiRouter.transitionService.onSuccess({to: 'base.configuration.**'}, (transition) => {
+        ActivitiesData.post({group: 'configuration', action: transition.targetState().name()});
+    });
+}
+
+export default angular
+    .module('ignite-console.configuration', [
+        uiValidate,
+        'asyncFilter',
+        generatorModule.name,
+        pageConfigure.name,
+        pageConfigureBasic.name,
+        pageConfigureAdvanced.name,
+        pageConfigureOverview.name,
+        pcUiGridFilters.name,
+        projectStructurePreview.name,
+        itemsTable.name,
+        pcValidation.name,
+        modalImportModels.name,
+        buttonImportModels.name,
+        buttonDownloadProject.name,
+        buttonPreviewProject.name,
+        previewPanel.name,
+        pcSplitButton.name,
+        uiAceJava.name,
+        uiAceSpring.name
+    ])
+    .config(registerStates)
+    .run(registerActivitiesHook)
+    .run(errorState)
+    .run(['ConfigEffects', 'ConfigureState', '$uiRouter', (ConfigEffects, ConfigureState, $uiRouter) => {
+        $uiRouter.plugin(UIRouterRx);
+
+        ConfigureState.addReducer(refsReducer({
+            models: {at: 'domains', store: 'caches'},
+            caches: {at: 'caches', store: 'models'}
+        }));
+
+        ConfigureState.addReducer((state, action) => Object.assign({}, state, {
+            clusterConfiguration: editReducer(state.clusterConfiguration, action),
+            configurationLoading: loadingReducer(state.configurationLoading, action),
+            basicCaches: itemsEditReducerFactory(basicCachesActionTypes)(state.basicCaches, action),
+            clusters: mapStoreReducerFactory(clustersActionTypes)(state.clusters, action),
+            shortClusters: mapCacheReducerFactory(shortClustersActionTypes)(state.shortClusters, action),
+            caches: mapStoreReducerFactory(cachesActionTypes)(state.caches, action),
+            shortCaches: mapCacheReducerFactory(shortCachesActionTypes)(state.shortCaches, action),
+            models: mapStoreReducerFactory(modelsActionTypes)(state.models, action),
+            shortModels: mapCacheReducerFactory(shortModelsActionTypes)(state.shortModels, action),
+            igfss: mapStoreReducerFactory(igfssActionTypes)(state.igfss, action),
+            shortIgfss: mapCacheReducerFactory(shortIGFSsActionTypes)(state.shortIgfss, action),
+            edit: editReducer2(state.edit, action)
+        }));
+
+        ConfigureState.addReducer(shortObjectsReducer);
+
+        ConfigureState.addReducer((state, action) => {
+            switch (action.type) {
+                case 'APPLY_ACTIONS_UNDO':
+                    return action.state;
+
+                default:
+                    return state;
+            }
+        });
+
+        const la = ConfigureState.actions$.pipe(scan((acc, action) => [...acc, action], []));
+
+        ConfigureState.actions$.pipe(
+            filter((a) => a.type === 'UNDO_ACTIONS'),
+            withLatestFrom(la, ({actions}, actionsWindow, initialState) => {
+                return {
+                    type: 'APPLY_ACTIONS_UNDO',
+                    state: actionsWindow.filter((a) => !actions.includes(a)).reduce(ConfigureState._combinedReducer, {})
+                };
+            }),
+            tap((a) => ConfigureState.dispatchAction(a))
+        )
+        .subscribe();
+        ConfigEffects.connect();
+    }])
+    .factory('configSelectionManager', ConfigSelectionManager)
+    .service('IgniteSummaryZipper', SummaryZipper)
+    .service('IgniteConfigurationResource', ConfigurationResource)
+    .service('ConfigSelectors', selectors)
+    .service('ConfigEffects', effects)
+    .service('ConfigChangesGuard', ConfigChangesGuard)
+    .service('PageConfigure', PageConfigure)
+    .service('ConfigureState', ConfigureState)
+    .service('ConfigurationDownload', ConfigurationDownload)
+    .service('Clusters', Clusters)
+    .service('Caches', Caches)
+    .service('IGFSs', IGFSs)
+    .service('Models', Models)
+    .directive('pcIsInCollection', isInCollection)
+    .directive('fakeUiCanExit', fakeUiCanExit)
+    .directive('formUiCanExitGuard', formUICanExitGuard)
+    .directive('igniteUiAceTabs', uiAceTabs)
+    .constant('JDBC_LINKS', JDBC_LINKS);
diff --git a/modules/frontend/app/configuration/mixins.pug b/modules/frontend/app/configuration/mixins.pug
new file mode 100644
index 0000000..f769c69
--- /dev/null
+++ b/modules/frontend/app/configuration/mixins.pug
@@ -0,0 +1,419 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/primitives/btn-group/index
+include /app/primitives/datepicker/index
+include /app/primitives/timepicker/index
+include /app/primitives/dropdown/index
+include /app/primitives/switcher/index
+include /app/primitives/form-field/index
+
+//- Function that convert enabled state to corresponding disabled state.
+-var enabledToDisabled = function (enabled) {
+-    return (enabled === false || enabled === true) ? !enabled : '!(' + enabled + ')';
+-}
+
+//- Mixin for XML and Java preview.
+mixin preview-xml-java(master, generator, detail)
+    ignite-ui-ace-tabs
+        .preview-panel(ng-init='mode = "spring"')
+            .preview-legend
+                a(ng-class='{active: mode === "spring"}' ng-click='mode = "spring"') Spring
+                a(ng-class='{active: mode === "java"}' ng-click='mode = "java"') Java
+                //a(ng-class='{active: mode === "app.config"}' ng-click='mode = "app.config"') app.config
+            .preview-content(ng-switch='mode')
+                ignite-ui-ace-spring(ng-switch-when='spring' data-master=master data-generator=generator ng-model='$parent.data' data-detail=detail)
+                ignite-ui-ace-java(ng-switch-when='java' data-master=master data-generator=generator ng-model='$parent.data' data-detail=detail)
+            .preview-content-empty(ng-if='!data')
+                label All Defaults
+
+mixin form-field__java-class({ label, model, name, disabled, required, tip, placeholder, validationActive })
+    -var errLbl = label.substring(0, label.length - 1)
+
+    +form-field__text({
+        label,
+        model,
+        name,
+        disabled,
+        required,
+        placeholder: placeholder || 'Enter fully qualified class name',
+        tip
+    })(
+        data-java-identifier='true'
+        data-java-package-specified='true'
+        data-java-keywords='true'
+        data-java-built-in-class='true'
+        data-validation-active=validationActive ? `{{ ${validationActive} }}` : `'always'`
+    )&attributes(attributes)
+        if  block
+            block
+
+        +form-field__error({ error: 'javaBuiltInClass', message: `${ errLbl } should not be the Java built-in class!` })
+        +form-field__error({ error: 'javaKeywords', message: `${ errLbl } could not contains reserved Java keyword!` })
+        +form-field__error({ error: 'javaPackageSpecified', message: `${ errLbl } does not have package specified!` })
+        +form-field__error({ error: 'javaIdentifier', message: `${ errLbl } is invalid Java identifier!` })
+
+//- Mixin for text field with enabled condition with options.
+mixin form-field__java-class--typeahead({ label, model, name, options, disabled, required, placeholder, tip, validationActive })
+    -var errLbl = label.substring(0, label.length - 1)
+
+    +form-field__typeahead({
+        label,
+        model,
+        name,
+        disabled,
+        required,
+        placeholder,
+        options,
+        tip
+    })&attributes(attributes)(
+        data-java-identifier='true'
+        data-java-package-specified='allow-built-in'
+        data-java-keywords='true'
+        data-validation-active=validationActive ? `{{ ${validationActive} }}` : `'always'`
+    )
+        +form-field__error({ error: 'javaKeywords', message: `${ errLbl } could not contains reserved Java keyword!` })
+        +form-field__error({ error: 'javaPackageSpecified', message: `${ errLbl } does not have package specified!` })
+        +form-field__error({ error: 'javaIdentifier', message: `${ errLbl } is invalid Java identifier!` })
+
+mixin form-field__java-package({ label, model, name, disabled, required, tip, tipOpts, placeholder })
+    +form-field__text({
+        label,
+        model,
+        name,
+        disabled,
+        required,
+        tip,
+        tipOpts,
+        placeholder
+    })(
+        data-java-keywords='true'
+        data-java-package-name='package-only'
+    )&attributes(attributes)
+        if  block
+            block
+
+        +form-field__error({ error: 'javaPackageName', message: 'Package name is invalid!' })
+        +form-field__error({ error: 'javaKeywords', message: 'Package name could not contains reserved java keyword!' })
+
+//- Mixin for text field with IP address check.
+mixin form-field__ip-address({ label, model, name, enabled, placeholder, tip })
+    +form-field__text({
+        label,
+        model,
+        name,
+        disabled: enabledToDisabled(enabled),
+        placeholder,
+        tip
+    })(data-ipaddress='true')
+        +form-field__error({ error: 'ipaddress', message: 'Invalid address!' })
+
+//- Mixin for text field with IP address and port range check.
+mixin form-field__ip-address-with-port-range({ label, model, name, enabled, placeholder, tip })
+    +form-field__text({
+        label,
+        model,
+        name,
+        disabled: enabledToDisabled(enabled),
+        placeholder,
+        tip
+    })(
+        data-ipaddress='true'
+        data-ipaddress-with-port='true'
+        data-ipaddress-with-port-range='true'
+    )
+        +form-field__error({ error: 'ipaddress', message: 'Invalid address!' })
+        +form-field__error({ error: 'ipaddressPort', message: 'Invalid port!' })
+        +form-field__error({ error: 'ipaddressPortRange', message: 'Invalid port range!' })
+
+//- Mixin for url field.
+mixin form-field__url({ label, model, name, enabled, required, placeholder, tip })
+    -var errLbl = label.substring(0, label.length - 1)
+
+    +form-field__text({
+        label,
+        model,
+        name,
+        disabled: enabledToDisabled(enabled),
+        required,
+        placeholder,
+        tip
+    })(
+        type='url'
+    )
+        if  block
+            block
+
+        +form-field__error({ error: 'url', message: `${ errLbl } should be a valid URL!` })
+
+mixin list-text-field({ items, lbl, name, itemName, itemsName })
+    list-editable(ng-model=items)&attributes(attributes)
+        list-editable-item-view
+            | {{ $item }}
+
+        list-editable-item-edit
+            +form-field__text({
+                label: lbl,
+                model: '$item',
+                name: `"${name}"`,
+                required: true,
+                placeholder: `Enter ${lbl.toLowerCase()}`
+            })(
+                ignite-unique=items
+                ignite-form-field-input-autofocus='true'
+            )
+                if  block
+                    block
+
+        list-editable-no-items
+            list-editable-add-item-button(
+                add-item=`$editLast((${items} = ${items} || []).push(''))`
+                label-single=itemName
+                label-multiple=itemsName
+            )
+
+mixin list-java-class-field(label, model, name, items)
+    +form-field__text({
+        label,
+        model,
+        name,
+        required: true,
+        placeholder: 'Enter fully qualified class name'
+    })(
+        java-identifier='true'
+        java-package-specified='true'
+        java-keywords='true'
+        java-built-in-class='true'
+
+        ignite-unique=items
+        ignite-form-field-input-autofocus='true'
+    )
+        +form-field__error({ error: 'javaBuiltInClass', message: `${ label } should not be the Java built-in class!` })
+        +form-field__error({ error: 'javaKeywords', message: `${ label } could not contains reserved Java keyword!` })
+        +form-field__error({ error: 'javaPackageSpecified', message: `${ label } does not have package specified!` })
+        +form-field__error({ error: 'javaIdentifier', message: `${ label } is invalid Java identifier!` })
+
+        if block
+            block
+
+mixin list-java-identifier-field(label, model, name, placeholder, items)
+    +form-field__text({
+        label,
+        model,
+        name,
+        required: true,
+        placeholder
+    })(
+        java-identifier='true'
+
+        ignite-unique=items
+        ignite-form-field-input-autofocus='true'
+    )
+        +form-field__error({ error: 'javaIdentifier', message: `${ label } is invalid Java identifier!` })
+
+        if block
+            block
+
+mixin list-url-field(label, model, name, items)
+    +form-field__text({
+        label,
+        model,
+        name,
+        required: true,
+        placeholder: 'Enter URL'
+    })(
+        type='url'
+
+        ignite-unique=items
+        ignite-form-field-input-autofocus='true'
+    )
+        +form-field__error({ error: 'url', message: 'URL should be valid!' })
+
+        if block
+            block
+
+mixin list-addresses({ items, name, tip, withPortRange = true })
+    list-editable(
+        ng-model=items
+        name=name
+        list-editable-cols=`::[{name: "Addresses:", tip: "${tip}"}]`
+    )&attributes(attributes)
+        list-editable-item-view {{ $item }}
+        list-editable-item-edit(item-name='address')
+            +form-field__text({
+                label: 'Address',
+                model: 'address',
+                name: '"address"',
+                required: true,
+                placeholder: 'IP address:port'
+            })(
+                ipaddress='true'
+                ipaddress-with-port='true'
+                ipaddress-with-port-range=withPortRange
+                ignite-unique=items
+                ignite-form-field-input-autofocus='true'
+            )
+                +form-field__error({ error: 'igniteUnique', message: 'Such IP address already exists!' })
+                +form-field__error({ error: 'ipaddress', message: 'Invalid address!' })
+                +form-field__error({ error: 'ipaddressPort', message: 'Invalid port!' })
+                +form-field__error({ error: 'ipaddressPortRange', message: 'Invalid port range!' })
+                +form-field__error({ error: 'required', message: 'IP address:port could not be empty!' })
+
+        list-editable-no-items
+            list-editable-add-item-button(
+                add-item=`$editLast((${items} = ${items} || []).push(""))`
+                label-multiple='addresses'
+                label-single='address'
+            )
+
+mixin form-field__cache-modes({ label, model, name, placeholder })
+    +form-field__dropdown({
+        label, model, name, placeholder,
+        options: '[\
+            {value: "LOCAL", label: "LOCAL"},\
+            {value: "REPLICATED", label: "REPLICATED"},\
+            {value: "PARTITIONED", label: "PARTITIONED"}\
+        ]',
+        tip: 'Cache modes:\
+        <ul>\
+            <li>PARTITIONED - in this mode the overall key set will be divided into partitions and all partitions will be split equally between participating nodes</li>\
+            <li>REPLICATED - in this mode all the keys are distributed to all participating nodes</li>\
+            <li>LOCAL - in this mode caches residing on different grid nodes will not know about each other</li>\
+        </ul>'
+    })&attributes(attributes)
+        if  block
+            block
+
+//- Mixin for eviction policy.
+mixin form-field__eviction-policy({ model, name, enabled, required, tip })
+    -var kind = model + '.kind'
+    -var policy = model + '[' + kind + ']'
+
+    .pc-form-grid-col-60
+        +form-field__dropdown({
+            label: 'Eviction policy:',
+            model: kind,
+            name: `${name}+"Kind"`,
+            disabled: enabledToDisabled(enabled),
+            required: required,
+            placeholder: '{{ ::$ctrl.Caches.evictionPolicy.kind.default }}',
+            options: '::$ctrl.Caches.evictionPolicy.values',
+            tip: tip
+        })
+    .pc-form-group.pc-form-grid-row(ng-if=kind)
+        .pc-form-grid-col-30
+            +form-field__number({
+                label: 'Batch size',
+                model: policy + '.batchSize',
+                name: name + '+ "batchSize"',
+                disabled: enabledToDisabled(enabled),
+                placeholder: '1',
+                min: '1',
+                tip: 'Number of entries to remove on shrink'
+            })
+        .pc-form-grid-col-30
+            form-field-size(
+                label='Max memory size:'
+                ng-model=`${policy}.maxMemorySize`
+                ng-model-options='{allowInvalid: true}'
+                name=`${name}.maxMemorySize`
+                ng-disabled=enabledToDisabled(enabled)
+                tip='Maximum allowed cache size'
+                placeholder='{{ ::$ctrl.Caches.evictionPolicy.maxMemorySize.default }}'
+                min=`{{ $ctrl.Caches.evictionPolicy.maxMemorySize.min(${model}) }}`
+                size-scale-label='mb'
+                size-type='bytes'
+            )
+                +form-field__error({ error: 'min', message: 'Either maximum memory size or maximum size should be greater than 0' })
+        .pc-form-grid-col-60
+            +form-field__number({
+                label: 'Max size:',
+                model: policy + '.maxSize',
+                name: name + '+ "maxSize"',
+                disabled: enabledToDisabled(enabled),
+                placeholder: '{{ ::$ctrl.Caches.evictionPolicy.maxSize.default }}',
+                min: `{{ $ctrl.Caches.evictionPolicy.maxSize.min(${model}) }}`,
+                tip: 'Maximum allowed size of cache before entry will start getting evicted'
+            })(
+                ng-model-options='{allowInvalid: true}'
+            )
+                +form-field__error({ error: 'min', message: 'Either maximum memory size or maximum size should be greater than 0' })
+
+mixin list-pair-edit({ items, keyLbl, valLbl, itemName, itemsName })
+    list-editable(ng-model=items)
+        list-editable-item-view
+            | {{ $item.name }} = {{ $item.value }}
+
+        list-editable-item-edit
+            - form = '$parent.form'
+            .pc-form-grid-row
+                .pc-form-grid-col-30(divider='=')
+                    +form-field__text({
+                        label: keyLbl,
+                        model: '$item.name',
+                        name: '"name"',
+                        required: true,
+                        placeholder: keyLbl
+                    })(
+                        ignite-unique=items
+                        ignite-unique-property='name'
+                        ignite-auto-focus
+                    )
+                        +form-field__error({ error: 'igniteUnique', message: 'Property with such name already exists!' })
+                .pc-form-grid-col-30
+                    +form-field__text({
+                        label: valLbl,
+                        model: '$item.value',
+                        name: '"value"',
+                        required: true,
+                        placeholder: valLbl
+                    })
+
+        list-editable-no-items
+            list-editable-add-item-button(
+                add-item=`$editLast((${items} = ${items} || []).push({}))`
+                label-single=itemName
+                label-multiple=itemsName
+            )
+
+mixin form-field__dialect({ label, model, name, required, tip, genericDialectName, placeholder, change })
+    +form-field__dropdown({
+        label,
+        model,
+        name,
+        required,
+        placeholder,
+        change,
+        options: '[\
+                {value: "Generic", label: "' + genericDialectName + '"},\
+                {value: "Oracle", label: "Oracle"},\
+                {value: "DB2", label: "IBM DB2"},\
+                {value: "SQLServer", label: "Microsoft SQL Server"},\
+                {value: "MySQL", label: "MySQL"},\
+                {value: "PostgreSQL", label: "PostgreSQL"},\
+                {value: "H2", label: "H2 database"}\
+        ]',
+        tip: `${ tip }
+            <ul>
+                <li>${ genericDialectName }</li>
+                <li>Oracle database</li>
+                <li>IBM DB2</li>
+                <li>Microsoft SQL Server</li>
+                <li>MySQL</li>
+                <li>PostgreSQL</li>
+                <li>H2 database</li>
+            </ul>`
+    })
diff --git a/modules/frontend/app/configuration/services/Caches.ts b/modules/frontend/app/configuration/services/Caches.ts
new file mode 100644
index 0000000..bf78771
--- /dev/null
+++ b/modules/frontend/app/configuration/services/Caches.ts
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import get from 'lodash/get';
+import ObjectID from 'bson-objectid';
+import omit from 'lodash/fp/omit';
+import {AtomicityModes, CacheModes, ShortCache} from '../types';
+import {Menu} from 'app/types';
+
+export default class Caches {
+    static $inject = ['$http', 'JDBC_LINKS'];
+
+    cacheModes: Menu<CacheModes> = [
+        {value: 'LOCAL', label: 'LOCAL'},
+        {value: 'REPLICATED', label: 'REPLICATED'},
+        {value: 'PARTITIONED', label: 'PARTITIONED'}
+    ];
+
+    atomicityModes: Menu<AtomicityModes> = [
+        {value: 'ATOMIC', label: 'ATOMIC'},
+        {value: 'TRANSACTIONAL', label: 'TRANSACTIONAL'},
+        {value: 'TRANSACTIONAL_SNAPSHOT', label: 'TRANSACTIONAL_SNAPSHOT'}
+    ];
+
+    constructor(private $http: ng.IHttpService, private JDBC_LINKS) {}
+
+    saveCache(cache) {
+        return this.$http.post('/api/v1/configuration/caches/save', cache);
+    }
+
+    getCache(cacheID: string) {
+        return this.$http.get(`/api/v1/configuration/caches/${cacheID}`);
+    }
+
+    removeCache(cacheID: string) {
+        return this.$http.post(`/api/v1/configuration/caches/remove/${cacheID}`);
+    }
+
+    getBlankCache() {
+        return {
+            _id: ObjectID.generate(),
+            evictionPolicy: {},
+            cacheMode: 'PARTITIONED',
+            atomicityMode: 'ATOMIC',
+            readFromBackup: true,
+            copyOnRead: true,
+            cacheStoreFactory: {
+                CacheJdbcBlobStoreFactory: {
+                    connectVia: 'DataSource'
+                },
+                CacheHibernateBlobStoreFactory: {
+                    hibernateProperties: []
+                }
+            },
+            writeBehindCoalescing: true,
+            nearConfiguration: {},
+            sqlFunctionClasses: [],
+            domains: [],
+            eagerTtl: true
+        };
+    }
+
+    toShortCache(cache: any): ShortCache {
+        return {
+            _id: cache._id,
+            name: cache.name,
+            backups: cache.backups,
+            cacheMode: cache.cacheMode,
+            atomicityMode: cache.atomicityMode
+        };
+    }
+
+    normalize = omit(['__v', 'space', 'clusters']);
+
+    nodeFilterKinds = [
+        {value: 'IGFS', label: 'IGFS nodes'},
+        {value: 'Custom', label: 'Custom'},
+        {value: null, label: 'Not set'}
+    ];
+
+    memoryModes = [
+        {value: 'ONHEAP_TIERED', label: 'ONHEAP_TIERED'},
+        {value: 'OFFHEAP_TIERED', label: 'OFFHEAP_TIERED'},
+        {value: 'OFFHEAP_VALUES', label: 'OFFHEAP_VALUES'}
+    ];
+
+    diskPageCompression = [
+        {value: 'SKIP_GARBAGE', label: 'SKIP_GARBAGE'},
+        {value: 'ZSTD', label: 'ZSTD'},
+        {value: 'LZ4', label: 'LZ4'},
+        {value: 'SNAPPY', label: 'SNAPPY'},
+        {value: null, label: 'Disabled'}
+    ];
+
+    offHeapMode = {
+        _val(cache) {
+            return (cache.offHeapMode === null || cache.offHeapMode === void 0) ? -1 : cache.offHeapMode;
+        },
+        onChange: (cache) => {
+            const offHeapMode = this.offHeapMode._val(cache);
+            switch (offHeapMode) {
+                case 1:
+                    return cache.offHeapMaxMemory = cache.offHeapMaxMemory > 0 ? cache.offHeapMaxMemory : null;
+                case 0:
+                case -1:
+                    return cache.offHeapMaxMemory = cache.offHeapMode;
+                default: break;
+            }
+        },
+        required: (cache) => cache.memoryMode === 'OFFHEAP_TIERED',
+        offheapDisabled: (cache) => !(cache.memoryMode === 'OFFHEAP_TIERED' && this.offHeapMode._val(cache) === -1),
+        default: 'Disabled'
+    };
+
+    offHeapModes = [
+        {value: -1, label: 'Disabled'},
+        {value: 1, label: 'Limited'},
+        {value: 0, label: 'Unlimited'}
+    ];
+
+    offHeapMaxMemory = {
+        min: 1
+    };
+
+    memoryMode = {
+        default: 'ONHEAP_TIERED',
+        offheapAndDomains: (cache) => {
+            return !(cache.memoryMode === 'OFFHEAP_VALUES' && cache.domains.length);
+        }
+    };
+
+    evictionPolicy = {
+        required: (cache) => {
+            return (cache.memoryMode || this.memoryMode.default) === 'ONHEAP_TIERED'
+                && cache.offHeapMaxMemory > 0
+                && !cache.evictionPolicy.kind;
+        },
+        values: [
+            {value: 'LRU', label: 'LRU'},
+            {value: 'FIFO', label: 'FIFO'},
+            {value: 'SORTED', label: 'Sorted'},
+            {value: null, label: 'Not set'}
+        ],
+        kind: {
+            default: 'Not set'
+        },
+        maxMemorySize: {
+            min: (evictionPolicy) => {
+                const policy = evictionPolicy[evictionPolicy.kind];
+
+                if (!policy)
+                    return true;
+
+                const maxSize = policy.maxSize === null || policy.maxSize === void 0
+                    ? this.evictionPolicy.maxSize.default
+                    : policy.maxSize;
+
+                return maxSize ? 0 : 1;
+            },
+            default: 0
+        },
+        maxSize: {
+            min: (evictionPolicy) => {
+                const policy = evictionPolicy[evictionPolicy.kind];
+
+                if (!policy)
+                    return true;
+
+                const maxMemorySize = policy.maxMemorySize === null || policy.maxMemorySize === void 0
+                    ? this.evictionPolicy.maxMemorySize.default
+                    : policy.maxMemorySize;
+
+                return maxMemorySize ? 0 : 1;
+            },
+            default: 100000
+        }
+    };
+
+    cacheStoreFactory = {
+        kind: {
+            default: 'Not set'
+        },
+        values: [
+            {value: 'CacheJdbcPojoStoreFactory', label: 'JDBC POJO store factory'},
+            {value: 'CacheJdbcBlobStoreFactory', label: 'JDBC BLOB store factory'},
+            {value: 'CacheHibernateBlobStoreFactory', label: 'Hibernate BLOB store factory'},
+            {value: null, label: 'Not set'}
+        ],
+        storeDisabledValueOff: (cache, value) => {
+            return cache && cache.cacheStoreFactory.kind ? true : !value;
+        },
+        storeEnabledReadOrWriteOn: (cache) => {
+            return cache && cache.cacheStoreFactory.kind ? (cache.readThrough || cache.writeThrough) : true;
+        }
+    };
+
+    writeBehindFlush = {
+        min: (cache) => {
+            return cache.writeBehindFlushSize === 0 && cache.writeBehindFlushFrequency === 0
+                ? 1
+                : 0;
+        }
+    };
+
+    getCacheBackupsCount(cache: ShortCache) {
+        return this.shouldShowCacheBackupsCount(cache)
+            ? (cache.backups || 0)
+            : void 0;
+    }
+
+    shouldShowCacheBackupsCount(cache: ShortCache) {
+        return cache && cache.cacheMode === 'PARTITIONED';
+    }
+
+    jdbcDriverURL(storeFactory) {
+        return this.JDBC_LINKS[get(storeFactory, 'dialect')];
+    }
+
+    requiresProprietaryDrivers(storeFactory) {
+        return !!this.jdbcDriverURL(storeFactory);
+    }
+}
diff --git a/modules/frontend/app/configuration/services/Clusters.spec.js b/modules/frontend/app/configuration/services/Clusters.spec.js
new file mode 100644
index 0000000..ffa0be1
--- /dev/null
+++ b/modules/frontend/app/configuration/services/Clusters.spec.js
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {suite, test} from 'mocha';
+import {assert} from 'chai';
+import {spy} from 'sinon';
+
+import Provider from './Clusters';
+
+const mocks = () => new Map([
+    ['$http', {
+        post: spy()
+    }]
+]);
+
+suite('Clusters service', () => {
+    test('discoveries', () => {
+        const s = new Provider(...mocks().values());
+        assert.isArray(s.discoveries, 'has discoveries array');
+        assert.isOk(s.discoveries.every((d) => d.value && d.label), 'discoveries have correct format');
+    });
+
+    test('minMemoryPolicySize', () => {
+        const s = new Provider(...mocks().values());
+        assert.isNumber(s.minMemoryPolicySize, 'has minMemoryPolicySize number');
+    });
+
+    test('saveCluster', () => {
+        const s = new Provider(...mocks().values());
+        const cluster = {id: 1, name: 'Test'};
+        s.saveCluster(cluster);
+        assert.isOk(s.$http.post.called, 'calls $http.post');
+        assert.equal(s.$http.post.lastCall.args[0], '/api/v1/configuration/clusters/save', 'uses correct API URL');
+        assert.deepEqual(s.$http.post.lastCall.args[1], cluster, 'sends cluster');
+    });
+
+    test('getBlankCluster', () => {
+        const s = new Provider(...mocks().values());
+        assert.isObject(s.getBlankCluster());
+    });
+});
diff --git a/modules/frontend/app/configuration/services/Clusters.ts b/modules/frontend/app/configuration/services/Clusters.ts
new file mode 100644
index 0000000..5e79a5e
--- /dev/null
+++ b/modules/frontend/app/configuration/services/Clusters.ts
@@ -0,0 +1,601 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import get from 'lodash/get';
+import find from 'lodash/find';
+import {from} from 'rxjs';
+import ObjectID from 'bson-objectid/objectid';
+import {uniqueName} from 'app/utils/uniqueName';
+import omit from 'lodash/fp/omit';
+import {DiscoveryKinds, FailoverSPIs, LoadBalancingKinds, ShortCluster, ShortDomainModel} from '../types';
+import {Menu} from 'app/types';
+
+const uniqueNameValidator = (defaultName = '') => (a, items = []) => {
+    return a && !items.some((b) => b._id !== a._id && (a.name || defaultName) === (b.name || defaultName));
+};
+
+export default class Clusters {
+    static $inject = ['$http', 'JDBC_LINKS'];
+
+    discoveries: Menu<DiscoveryKinds> = [
+        {value: 'Vm', label: 'Static IPs'},
+        {value: 'Multicast', label: 'Multicast'},
+        {value: 'S3', label: 'AWS S3'},
+        {value: 'Cloud', label: 'Apache jclouds'},
+        {value: 'GoogleStorage', label: 'Google cloud storage'},
+        {value: 'Jdbc', label: 'JDBC'},
+        {value: 'SharedFs', label: 'Shared filesystem'},
+        {value: 'ZooKeeper', label: 'Apache ZooKeeper'},
+        {value: 'Kubernetes', label: 'Kubernetes'}
+    ];
+
+    minMemoryPolicySize = 10485760; // In bytes
+    ackSendThreshold = {
+        min: 1,
+        default: 16
+    };
+    messageQueueLimit = {
+        min: 0,
+        default: 1024
+    };
+    unacknowledgedMessagesBufferSize = {
+        min: (
+            currentValue = this.unacknowledgedMessagesBufferSize.default,
+            messageQueueLimit = this.messageQueueLimit.default,
+            ackSendThreshold = this.ackSendThreshold.default
+        ) => {
+            if (currentValue === this.unacknowledgedMessagesBufferSize.default)
+                return currentValue;
+
+            const {validRatio} = this.unacknowledgedMessagesBufferSize;
+            return Math.max(messageQueueLimit * validRatio, ackSendThreshold * validRatio);
+        },
+        validRatio: 5,
+        default: 0
+    };
+    sharedMemoryPort = {
+        default: 48100,
+        min: -1,
+        max: 65535,
+        invalidValues: [0]
+    };
+
+    /**
+     * Cluster-related configuration stuff
+     */
+    constructor(private $http: ng.IHttpService, private JDBC_LINKS) {}
+
+    getConfiguration(clusterID: string) {
+        return this.$http.get(`/api/v1/configuration/${clusterID}`);
+    }
+
+    getAllConfigurations() {
+        return this.$http.get('/api/v1/configuration/list');
+    }
+
+    getCluster(clusterID: string) {
+        return this.$http.get(`/api/v1/configuration/clusters/${clusterID}`);
+    }
+
+    getClusterCaches(clusterID: string) {
+        return this.$http.get(`/api/v1/configuration/clusters/${clusterID}/caches`);
+    }
+
+    getClusterModels(clusterID: string) {
+        return this.$http.get<{data: ShortDomainModel[]}>(`/api/v1/configuration/clusters/${clusterID}/models`);
+    }
+
+    getClusterIGFSs(clusterID) {
+        return this.$http.get(`/api/v1/configuration/clusters/${clusterID}/igfss`);
+    }
+
+    getClustersOverview() {
+        return this.$http.get<{data: ShortCluster[]}>('/api/v1/configuration/clusters/');
+    }
+
+    getClustersOverview$() {
+        return from(this.getClustersOverview());
+    }
+
+    saveCluster(cluster) {
+        return this.$http.post('/api/v1/configuration/clusters/save', cluster);
+    }
+
+    saveCluster$(cluster) {
+        return from(this.saveCluster(cluster));
+    }
+
+    removeCluster(cluster) {
+        return this.$http.post('/api/v1/configuration/clusters/remove', {_id: cluster});
+    }
+
+    removeCluster$(cluster) {
+        return from(this.removeCluster(cluster));
+    }
+
+    saveBasic(changedItems) {
+        return this.$http.put('/api/v1/configuration/clusters/basic', changedItems);
+    }
+
+    saveAdvanced(changedItems) {
+        return this.$http.put('/api/v1/configuration/clusters/', changedItems);
+    }
+
+    getBlankCluster() {
+        return {
+            _id: ObjectID.generate(),
+            activeOnStart: true,
+            cacheSanityCheckEnabled: true,
+            atomicConfiguration: {},
+            cacheKeyConfiguration: [],
+            deploymentSpi: {
+                URI: {
+                    uriList: [],
+                    scanners: []
+                }
+            },
+            marshaller: {},
+            peerClassLoadingLocalClassPathExclude: [],
+            sslContextFactory: {
+                trustManagers: []
+            },
+            swapSpaceSpi: {},
+            transactionConfiguration: {},
+            dataStorageConfiguration: {
+                pageSize: null,
+                concurrencyLevel: null,
+                defaultDataRegionConfiguration: {
+                    name: 'default'
+                },
+                dataRegionConfigurations: []
+            },
+            memoryConfiguration: {
+                pageSize: null,
+                memoryPolicies: [{
+                    name: 'default',
+                    maxSize: null
+                }]
+            },
+            hadoopConfiguration: {
+                nativeLibraryNames: []
+            },
+            serviceConfigurations: [],
+            executorConfiguration: [],
+            sqlConnectorConfiguration: {
+                tcpNoDelay: true
+            },
+            clientConnectorConfiguration: {
+                tcpNoDelay: true,
+                jdbcEnabled: true,
+                odbcEnabled: true,
+                thinClientEnabled: true,
+                useIgniteSslContextFactory: true
+            },
+            space: void 0,
+            discovery: {
+                kind: 'Multicast',
+                Vm: {addresses: ['127.0.0.1:47500..47510']},
+                Multicast: {addresses: ['127.0.0.1:47500..47510']},
+                Jdbc: {initSchema: true},
+                Cloud: {regions: [], zones: []}
+            },
+            binaryConfiguration: {typeConfigurations: [], compactFooter: true},
+            communication: {tcpNoDelay: true},
+            connector: {noDelay: true},
+            collision: {kind: 'Noop', JobStealing: {stealingEnabled: true}, PriorityQueue: {starvationPreventionEnabled: true}},
+            failoverSpi: [],
+            logger: {Log4j: { mode: 'Default'}},
+            caches: [],
+            igfss: [],
+            models: [],
+            checkpointSpi: [],
+            loadBalancingSpi: [],
+            autoActivationEnabled: true
+        };
+    }
+
+    failoverSpis: Menu<FailoverSPIs> = [
+        {value: 'JobStealing', label: 'Job stealing'},
+        {value: 'Never', label: 'Never'},
+        {value: 'Always', label: 'Always'},
+        {value: 'Custom', label: 'Custom'}
+    ];
+
+    toShortCluster(cluster) {
+        return {
+            _id: cluster._id,
+            name: cluster.name,
+            discovery: cluster.discovery.kind,
+            cachesCount: (cluster.caches || []).length,
+            modelsCount: (cluster.models || []).length,
+            igfsCount: (cluster.igfss || []).length
+        };
+    }
+
+    jdbcDriverURL(dataSrc) {
+        return this.JDBC_LINKS[get(dataSrc, 'dialect')];
+    }
+
+    requiresProprietaryDrivers(dataSrc) {
+        return !!this.jdbcDriverURL(dataSrc);
+    }
+
+    dataRegion = {
+        name: {
+            default: 'default',
+            invalidValues: ['sysMemPlc']
+        },
+        initialSize: {
+            default: 268435456,
+            min: 10485760
+        },
+        maxSize: {
+            default: '0.2 * totalMemoryAvailable',
+            min: (dataRegion) => {
+                if (!dataRegion)
+                    return;
+
+                return dataRegion.initialSize || this.dataRegion.initialSize.default;
+            }
+        },
+        evictionThreshold: {
+            step: 0.001,
+            max: 0.999,
+            min: 0.5,
+            default: 0.9
+        },
+        emptyPagesPoolSize: {
+            default: 100,
+            min: 11,
+            max: (cluster, dataRegion) => {
+                if (!cluster || !dataRegion || !dataRegion.maxSize)
+                    return;
+
+                const perThreadLimit = 10; // Took from Ignite
+                const maxSize = dataRegion.maxSize;
+                const pageSize = cluster.dataStorageConfiguration.pageSize || this.dataStorageConfiguration.pageSize.default;
+                const maxPoolSize = Math.floor(maxSize / pageSize / perThreadLimit);
+
+                return maxPoolSize;
+            }
+        },
+        metricsSubIntervalCount: {
+            default: 5,
+            min: 1,
+            step: 1
+        },
+        metricsRateTimeInterval: {
+            min: 1000,
+            default: 60000,
+            step: 1000
+        }
+    };
+
+    makeBlankDataRegionConfiguration() {
+        return {_id: ObjectID.generate()};
+    }
+
+    addDataRegionConfiguration(cluster) {
+        const dataRegionConfigurations = get(cluster, 'dataStorageConfiguration.dataRegionConfigurations');
+
+        if (!dataRegionConfigurations)
+            return;
+
+        return dataRegionConfigurations.push(Object.assign(this.makeBlankDataRegionConfiguration(), {
+            name: uniqueName('New data region', dataRegionConfigurations.concat(cluster.dataStorageConfiguration.defaultDataRegionConfiguration))
+        }));
+    }
+
+    memoryPolicy = {
+        name: {
+            default: 'default',
+            invalidValues: ['sysMemPlc']
+        },
+        initialSize: {
+            default: 268435456,
+            min: 10485760
+        },
+        maxSize: {
+            default: '0.8 * totalMemoryAvailable',
+            min: (memoryPolicy) => {
+                return memoryPolicy.initialSize || this.memoryPolicy.initialSize.default;
+            }
+        },
+        customValidators: {
+            defaultMemoryPolicyExists: (name, items = []) => {
+                const def = this.memoryPolicy.name.default;
+                const normalizedName = (name || def);
+
+                if (normalizedName === def)
+                    return true;
+
+                return items.some((policy) => (policy.name || def) === normalizedName);
+            },
+            uniqueMemoryPolicyName: (a, items = []) => {
+                const def = this.memoryPolicy.name.default;
+                return !items.some((b) => b._id !== a._id && (a.name || def) === (b.name || def));
+            }
+        },
+        emptyPagesPoolSize: {
+            default: 100,
+            min: 11,
+            max: (cluster, memoryPolicy) => {
+                if (!memoryPolicy || !memoryPolicy.maxSize)
+                    return;
+
+                const perThreadLimit = 10; // Took from Ignite
+                const maxSize = memoryPolicy.maxSize;
+                const pageSize = cluster.memoryConfiguration.pageSize || this.memoryConfiguration.pageSize.default;
+                const maxPoolSize = Math.floor(maxSize / pageSize / perThreadLimit);
+
+                return maxPoolSize;
+            }
+        }
+    };
+
+    getDefaultClusterMemoryPolicy(cluster) {
+        const def = this.memoryPolicy.name.default;
+        const normalizedName = get(cluster, 'memoryConfiguration.defaultMemoryPolicyName') || def;
+        return get(cluster, 'memoryConfiguration.memoryPolicies', []).find((p) => {
+            return (p.name || def) === normalizedName;
+        });
+    }
+
+    makeBlankCheckpointSPI() {
+        return {
+            FS: {
+                directoryPaths: []
+            },
+            S3: {
+                awsCredentials: {
+                    kind: 'Basic'
+                },
+                clientConfiguration: {
+                    retryPolicy: {
+                        kind: 'Default'
+                    },
+                    useReaper: true
+                }
+            }
+        };
+    }
+
+    addCheckpointSPI(cluster) {
+        const item = this.makeBlankCheckpointSPI();
+        cluster.checkpointSpi.push(item);
+        return item;
+    }
+
+    makeBlankLoadBalancingSpi() {
+        return {
+            Adaptive: {
+                loadProbe: {
+                    Job: {useAverage: true},
+                    CPU: {
+                        useAverage: true,
+                        useProcessors: true
+                    },
+                    ProcessingTime: {useAverage: true}
+                }
+            }
+        };
+    }
+
+    addLoadBalancingSpi(cluster) {
+        return cluster.loadBalancingSpi.push(this.makeBlankLoadBalancingSpi());
+    }
+
+    loadBalancingKinds: Menu<LoadBalancingKinds> = [
+        {value: 'RoundRobin', label: 'Round-robin'},
+        {value: 'Adaptive', label: 'Adaptive'},
+        {value: 'WeightedRandom', label: 'Random'},
+        {value: 'Custom', label: 'Custom'}
+    ];
+
+    makeBlankMemoryPolicy() {
+        return {_id: ObjectID.generate()};
+    }
+
+    addMemoryPolicy(cluster) {
+        const memoryPolicies = get(cluster, 'memoryConfiguration.memoryPolicies');
+
+        if (!memoryPolicies)
+            return;
+
+        return memoryPolicies.push(Object.assign(this.makeBlankMemoryPolicy(), {
+            // Blank name for default policy if there are not other policies
+            name: memoryPolicies.length ? uniqueName('New memory policy', memoryPolicies) : ''
+        }));
+    }
+
+    // For versions 2.1-2.2, use dataStorageConfiguration since 2.3
+    memoryConfiguration = {
+        pageSize: {
+            default: 1024 * 2,
+            values: [
+                {value: null, label: 'Default (2kb)'},
+                {value: 1024 * 1, label: '1 kb'},
+                {value: 1024 * 2, label: '2 kb'},
+                {value: 1024 * 4, label: '4 kb'},
+                {value: 1024 * 8, label: '8 kb'},
+                {value: 1024 * 16, label: '16 kb'}
+            ]
+        },
+        systemCacheInitialSize: {
+            default: 41943040,
+            min: 10485760
+        },
+        systemCacheMaxSize: {
+            default: 104857600,
+            min: (cluster) => {
+                return get(cluster, 'memoryConfiguration.systemCacheInitialSize') || this.memoryConfiguration.systemCacheInitialSize.default;
+            }
+        }
+    };
+
+    // Added in 2.3
+    dataStorageConfiguration = {
+        pageSize: {
+            default: 1024 * 4,
+            values: [
+                {value: null, label: 'Default (4kb)'},
+                {value: 1024 * 1, label: '1 kb'},
+                {value: 1024 * 2, label: '2 kb'},
+                {value: 1024 * 4, label: '4 kb'},
+                {value: 1024 * 8, label: '8 kb'},
+                {value: 1024 * 16, label: '16 kb'}
+            ]
+        },
+        systemRegionInitialSize: {
+            default: 41943040,
+            min: 10485760
+        },
+        systemRegionMaxSize: {
+            default: 104857600,
+            min: (cluster) => {
+                return get(cluster, 'dataStorageConfiguration.systemRegionInitialSize') || this.dataStorageConfiguration.systemRegionInitialSize.default;
+            }
+        }
+    };
+
+    persistenceEnabled(dataStorage) {
+        return !!(get(dataStorage, 'defaultDataRegionConfiguration.persistenceEnabled')
+            || find(get(dataStorage, 'dataRegionConfigurations'), (storeCfg) => storeCfg.persistenceEnabled));
+    }
+
+    swapSpaceSpi = {
+        readStripesNumber: {
+            default: 'availableProcessors',
+            customValidators: {
+                powerOfTwo: (value) => {
+                    return !value || ((value & -value) === value);
+                }
+            }
+        }
+    };
+
+    makeBlankServiceConfiguration() {
+        return {_id: ObjectID.generate()};
+    }
+
+    addServiceConfiguration(cluster) {
+        if (!cluster.serviceConfigurations) cluster.serviceConfigurations = [];
+        cluster.serviceConfigurations.push(Object.assign(this.makeBlankServiceConfiguration(), {
+            name: uniqueName('New service configuration', cluster.serviceConfigurations)
+        }));
+    }
+
+    serviceConfigurations = {
+        serviceConfiguration: {
+            name: {
+                customValidators: {
+                    uniqueName: uniqueNameValidator('')
+                }
+            }
+        }
+    };
+
+    systemThreadPoolSize = {
+        default: 'max(8, availableProcessors) * 2',
+        min: 2
+    };
+
+    rebalanceThreadPoolSize = {
+        default: 1,
+        min: 1,
+        max: (cluster) => {
+            return cluster.systemThreadPoolSize ? cluster.systemThreadPoolSize - 1 : void 0;
+        }
+    };
+
+    addExecutorConfiguration(cluster) {
+        if (!cluster.executorConfiguration) cluster.executorConfiguration = [];
+        const item = {_id: ObjectID.generate(), name: ''};
+        cluster.executorConfiguration.push(item);
+        return item;
+    }
+
+    executorConfigurations = {
+        allNamesExist: (executorConfigurations = []) => {
+            return executorConfigurations.every((ec) => ec && ec.name);
+        },
+        allNamesUnique: (executorConfigurations = []) => {
+            const uniqueNames = new Set(executorConfigurations.map((ec) => ec.name));
+            return uniqueNames.size === executorConfigurations.length;
+        }
+    };
+
+    executorConfiguration = {
+        name: {
+            customValidators: {
+                uniqueName: uniqueNameValidator()
+            }
+        }
+    };
+
+    marshaller = {
+        kind: {
+            default: 'BinaryMarshaller'
+        }
+    };
+
+    odbc = {
+        odbcEnabled: {
+            correctMarshaller: (cluster, odbcEnabled) => {
+                const marshallerKind = get(cluster, 'marshaller.kind') || this.marshaller.kind.default;
+                return !odbcEnabled || marshallerKind === this.marshaller.kind.default;
+            },
+            correctMarshallerWatch: (root) => `${root}.marshaller.kind`
+        }
+    };
+
+    swapSpaceSpis = [
+        {value: 'FileSwapSpaceSpi', label: 'File-based swap'},
+        {value: null, label: 'Not set'}
+    ];
+
+    affinityFunctions = [
+        {value: 'Rendezvous', label: 'Rendezvous'},
+        {value: 'Custom', label: 'Custom'},
+        {value: null, label: 'Default'}
+    ];
+
+    normalize = omit(['__v', 'space']);
+
+    addPeerClassLoadingLocalClassPathExclude(cluster) {
+        if (!cluster.peerClassLoadingLocalClassPathExclude) cluster.peerClassLoadingLocalClassPathExclude = [];
+        return cluster.peerClassLoadingLocalClassPathExclude.push('');
+    }
+
+    addBinaryTypeConfiguration(cluster) {
+        if (!cluster.binaryConfiguration.typeConfigurations) cluster.binaryConfiguration.typeConfigurations = [];
+        const item = {_id: ObjectID.generate()};
+        cluster.binaryConfiguration.typeConfigurations.push(item);
+        return item;
+    }
+
+    addLocalEventListener(cluster) {
+        if (!cluster.localEventListeners)
+            cluster.localEventListeners = [];
+
+        const item = {_id: ObjectID.generate()};
+
+        cluster.localEventListeners.push(item);
+
+        return item;
+    }
+}
diff --git a/modules/frontend/app/configuration/services/ConfigChangesGuard.spec.js b/modules/frontend/app/configuration/services/ConfigChangesGuard.spec.js
new file mode 100644
index 0000000..c703579
--- /dev/null
+++ b/modules/frontend/app/configuration/services/ConfigChangesGuard.spec.js
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {assert} from 'chai';
+import {IgniteObjectDiffer} from './ConfigChangesGuard';
+
+suite('Config changes guard', () => {
+    test('Object differ', () => {
+        const differ = new IgniteObjectDiffer();
+
+        assert.isUndefined(
+            differ.diff({a: void 0}, {a: false}),
+            'No changes when boolean values changes from undefined to false'
+        );
+
+        assert.isUndefined(
+            differ.diff({a: void 0}, {a: null}),
+            'No changes when undefined value changes to null'
+        );
+
+        assert.isUndefined(
+            differ.diff({a: void 0}, {a: ''}),
+            'No changes when undefined value changes to an empty string'
+        );
+    });
+});
diff --git a/modules/frontend/app/configuration/services/ConfigChangesGuard.ts b/modules/frontend/app/configuration/services/ConfigChangesGuard.ts
new file mode 100644
index 0000000..87dc6d3
--- /dev/null
+++ b/modules/frontend/app/configuration/services/ConfigChangesGuard.ts
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {of} from 'rxjs';
+import {catchError, switchMap} from 'rxjs/operators';
+import {Confirm} from 'app/services/Confirm.service';
+import {DiffPatcher} from 'jsondiffpatch';
+import {html} from 'jsondiffpatch/public/build/jsondiffpatch-formatters.js';
+import 'jsondiffpatch/public/formatters-styles/html.css';
+
+export class IgniteObjectDiffer<T> {
+    diffPatcher: DiffPatcher;
+
+    constructor() {
+        this.diffPatcher = new DiffPatcher({
+            cloneDiffValues: true
+        });
+
+        const shouldSkip = (val) => val === null || val === void 0 || val === '';
+
+        function igniteConfigFalsyFilter(context) {
+            // Special logic for checkboxes.
+            if (shouldSkip(context.left) && context.right === false)
+                delete context.right;
+
+            if (shouldSkip(context.left))
+                delete context.left;
+
+            if (shouldSkip(context.right))
+                delete context.right;
+        }
+
+        igniteConfigFalsyFilter.filterName = 'igniteConfigFalsy';
+
+        this.diffPatcher.processor.pipes.diff.before('trivial', igniteConfigFalsyFilter);
+    }
+
+    diff(a: T, b: T) {
+        return this.diffPatcher.diff(a, b);
+    }
+}
+
+export default class ConfigChangesGuard<T> {
+    static $inject = ['Confirm', '$sce'];
+
+    constructor(private Confirm: Confirm, private $sce: ng.ISCEService) {}
+
+    differ = new IgniteObjectDiffer<T>();
+
+    _hasChanges(a: T, b: T) {
+        return this.differ.diff(a, b);
+    }
+
+    _confirm(changes) {
+        return this.Confirm.confirm(this.$sce.trustAsHtml(`
+            <p>
+            You have unsaved changes.
+            Are you sure you want to discard them?
+            </p>
+            <details class='config-changes-guard__details'>
+                <summary>Click here to see changes</summary>
+                <div style='max-height: 400px; overflow: auto;'>${html.format(changes)}</div>                
+            </details>
+        `));
+    }
+
+    /**
+     * Compares values and asks user if he wants to continue.
+     *
+     * @param a Left comparison value.
+     * @param b Right comparison value.
+     */
+    guard(a: T, b: T) {
+        if (!a && !b)
+            return Promise.resolve(true);
+
+        return of(this._hasChanges(a, b)).pipe(
+            switchMap((changes) => changes ? this._confirm(changes).then(() => true) : of(true)),
+            catchError(() => of(false))
+        ).toPromise();
+    }
+}
diff --git a/modules/frontend/app/configuration/services/ConfigSelectionManager.ts b/modules/frontend/app/configuration/services/ConfigSelectionManager.ts
new file mode 100644
index 0000000..e121587
--- /dev/null
+++ b/modules/frontend/app/configuration/services/ConfigSelectionManager.ts
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {merge, Observable} from 'rxjs';
+import {distinctUntilChanged, filter, map, mapTo, pluck, share, startWith, withLatestFrom} from 'rxjs/operators';
+import {RejectType, TransitionService} from '@uirouter/angularjs';
+import isEqual from 'lodash/isEqual';
+
+configSelectionManager.$inject = ['$transitions'];
+export default function configSelectionManager($transitions: TransitionService) {
+    /**
+     * Determines what items should be marked as selected and if something is being edited at the moment.
+     */
+    return ({itemID$, selectedItemRows$, visibleRows$, loadedItems$}) => {
+        // Aborted transitions happen when form has unsaved changes, user attempts to leave
+        // but decides to stay after screen asks for leave confirmation.
+        const abortedTransitions$ = Observable.create((observer) => {
+            return $transitions.onError({}, (t) => observer.next(t));
+        }).pipe(filter((t) => t.error().type === RejectType.ABORTED));
+
+        const firstItemID$ = visibleRows$.pipe(
+            withLatestFrom(itemID$, loadedItems$),
+            filter(([rows, id, items]) => !id && rows && rows.length === items.length),
+            pluck('0', '0', 'entity', '_id')
+        );
+
+        const selectedItemRowsIDs$ = selectedItemRows$.pipe(map((rows) => rows.map((r) => r._id)), share());
+        const singleSelectionEdit$ = selectedItemRows$.pipe(filter((r) => r && r.length === 1), pluck('0', '_id'));
+        const selectedMultipleOrNone$ = selectedItemRows$.pipe(filter((r) => r.length > 1 || r.length === 0));
+        const loadedItemIDs$ = loadedItems$.pipe(map((rows) => new Set(rows.map((r) => r._id))), share());
+        const currentItemWasRemoved$ = loadedItemIDs$.pipe(
+            withLatestFrom(
+                itemID$.pipe(filter((v) => v && v !== 'new')),
+                /**
+                 * Without startWith currentItemWasRemoved$ won't emit in the following scenario:
+                 * 1. User opens items page (no item id in location).
+                 * 2. Selection manager commands to edit first item.
+                 * 3. User removes said item.
+                 */
+                selectedItemRowsIDs$.pipe(startWith([]))
+            ),
+            filter(([existingIDs, itemID, selectedIDs]) => !existingIDs.has(itemID)),
+            map(([existingIDs, itemID, selectedIDs]) => selectedIDs.filter((id) => id !== itemID)),
+            share()
+        );
+
+        // Edit first loaded item or when there's only one item selected
+        const editGoes$ = merge(firstItemID$, singleSelectionEdit$).pipe(
+            // Don't go to non-existing items.
+            // Happens when user naviagtes to older history and some items were already removed.
+            withLatestFrom(loadedItemIDs$),
+            filter(([id, loaded]) => id && loaded.has(id)),
+            pluck('0')
+        );
+        // Stop edit when multiple or none items are selected or when current item was removed
+        const editLeaves$ = merge(
+            selectedMultipleOrNone$.pipe(mapTo({})),
+            currentItemWasRemoved$.pipe(mapTo({location: 'replace', custom: {justIDUpdate: true}}))
+        ).pipe(share());
+
+        const selectedItemIDs$ = merge(
+            // Select nothing when creating an item or select current item
+            itemID$.pipe(filter((id) => id), map((id) => id === 'new' ? [] : [id])),
+            // Restore previous item selection when transition gets aborted
+            abortedTransitions$.pipe(withLatestFrom(itemID$, (_, id) => [id])),
+            // Select all incoming selected rows
+            selectedItemRowsIDs$
+        ).pipe(
+            // If nothing's selected and there are zero rows, ui-grid will behave as if all rows are selected
+            startWith([]),
+            // Some scenarios cause same item to be selected multiple times in a row,
+            // so it makes sense to filter out duplicate entries
+            distinctUntilChanged(isEqual),
+            share()
+        );
+
+        return {selectedItemIDs$, editGoes$, editLeaves$};
+    };
+}
diff --git a/modules/frontend/app/configuration/services/ConfigurationDownload.spec.js b/modules/frontend/app/configuration/services/ConfigurationDownload.spec.js
new file mode 100644
index 0000000..9c949d6
--- /dev/null
+++ b/modules/frontend/app/configuration/services/ConfigurationDownload.spec.js
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Provider from './ConfigurationDownload';
+
+import {suite, test} from 'mocha';
+import {assert} from 'chai';
+import {spy} from 'sinon';
+
+const mocks = () => new Map([
+    ['messages', {
+        showError: spy()
+    }],
+    ['activitiesData', {
+        post: spy()
+    }],
+    ['configuration', {
+        populate: (value) => Promise.resolve(value),
+        _clusters: [],
+        read() {
+            return Promise.resolve({clusters: this._clusters});
+        }
+    }],
+    ['summaryZipper', spy((value) => Promise.resolve(value))],
+    ['Version', {
+        currentSbj: {
+            getValue() {
+                return '2.0';
+            }
+        }
+    }],
+    ['$q', Promise],
+    ['$rootScope', {
+        IgniteDemoMode: true
+    }],
+    ['PageConfigure', {
+        getClusterConfiguration: () => Promise.resolve({clusters: [{_id: 1, name: 'An Cluster'}]})
+    }],
+    ['IgniteConfigurationResource', {
+        populate: () => Promise.resolve({clusters: []})
+    }]
+]);
+
+const saverMock = () => ({
+    saveAs: spy()
+});
+
+suite('page-configure, ConfigurationDownload service', () => {
+    test('fails and shows error message when summary zipper fails', () => {
+        const service = new Provider(...mocks().values());
+        const cluster = {_id: 1, name: 'An Cluster'};
+        service.configuration._clusters = [cluster];
+        service.summaryZipper = () => Promise.reject({message: 'Summary zipper failed.'});
+
+        return service.downloadClusterConfiguration(cluster)
+        .then(() => Promise.reject('Should not happen'))
+        .catch(() => {
+            assert.equal(
+                service.messages.showError.getCall(0).args[0],
+                'Failed to generate project files. Summary zipper failed.',
+                'shows correct error message when summary zipper fails'
+            );
+        });
+    });
+
+    test('calls correct dependcies', () => {
+        const service = new Provider(...mocks().values());
+        service.saver = saverMock();
+        const cluster = {_id: 1, name: 'An Cluster'};
+        service.configuration._clusters = [cluster];
+
+        return service.downloadClusterConfiguration(cluster)
+        .then(() => {
+            assert.deepEqual(
+                service.activitiesData.post.getCall(0).args[0],
+                {action: '/configuration/download'},
+                'submits activity data'
+            );
+            assert.deepEqual(service.summaryZipper.getCall(0).args, [{
+                cluster,
+                data: {},
+                IgniteDemoMode: true,
+                targetVer: '2.0'
+            }], 'summary zipper arguments are correct');
+            assert.deepEqual(service.saver.saveAs.getCall(0).args, [
+                {
+                    cluster,
+                    data: {},
+                    IgniteDemoMode: true,
+                    targetVer: '2.0'
+                },
+                'An_Cluster-project.zip'
+            ], 'saver arguments are correct');
+        });
+    });
+});
diff --git a/modules/frontend/app/configuration/services/ConfigurationDownload.ts b/modules/frontend/app/configuration/services/ConfigurationDownload.ts
new file mode 100644
index 0000000..a93be29
--- /dev/null
+++ b/modules/frontend/app/configuration/services/ConfigurationDownload.ts
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import saver from 'file-saver';
+import {ClusterLike} from '../types';
+import MessagesFactory from 'app/services/Messages.service';
+import Activities from 'app/core/activities/Activities.data';
+import ConfigurationResource from './ConfigurationResource';
+import SummaryZipper from './SummaryZipper';
+import Version from 'app/services/Version.service';
+import PageConfigure from './PageConfigure';
+
+export default class ConfigurationDownload {
+    static $inject = [
+        'IgniteMessages',
+        'IgniteActivitiesData',
+        'IgniteConfigurationResource',
+        'IgniteSummaryZipper',
+        'IgniteVersion',
+        '$q',
+        '$rootScope',
+        'PageConfigure'
+    ];
+
+    constructor(
+        private messages: ReturnType<typeof MessagesFactory>,
+        private activitiesData: Activities,
+        private configuration: ConfigurationResource,
+        private summaryZipper: SummaryZipper,
+        private Version: Version,
+        private $q: ng.IQService,
+        private $rootScope: ng.IRootScopeService & {IgniteDemoMode: boolean},
+        private PageConfigure: PageConfigure
+    ) {}
+
+    saver = saver;
+
+    downloadClusterConfiguration(cluster: ClusterLike) {
+        this.activitiesData.post({action: '/configuration/download'});
+
+        return this.PageConfigure.getClusterConfiguration({clusterID: cluster._id, isDemo: !!this.$rootScope.IgniteDemoMode})
+            .then((data) => this.configuration.populate(data))
+            .then(({clusters}) => {
+                return clusters.find(({_id}) => _id === cluster._id)
+                    || this.$q.reject({message: `Cluster ${cluster.name} not found`});
+            })
+            .then((cluster) => {
+                return this.summaryZipper({
+                    cluster,
+                    data: {},
+                    IgniteDemoMode: this.$rootScope.IgniteDemoMode,
+                    targetVer: this.Version.currentSbj.getValue()
+                });
+            })
+            .then((data) => this.saver.saveAs(data, this.nameFile(cluster)))
+            .catch((e) => (
+                this.messages.showError(`Failed to generate project files. ${e.message}`)
+            ));
+    }
+
+    nameFile(cluster: ClusterLike) {
+        return `${this.escapeFileName(cluster.name)}-project.zip`;
+    }
+
+    escapeFileName(name: string) {
+        return name.replace(/[\\\/*\"\[\],\.:;|=<>?]/g, '-').replace(/ /g, '_');
+    }
+}
diff --git a/modules/frontend/app/configuration/services/ConfigurationResource.spec.js b/modules/frontend/app/configuration/services/ConfigurationResource.spec.js
new file mode 100644
index 0000000..47fdc91
--- /dev/null
+++ b/modules/frontend/app/configuration/services/ConfigurationResource.spec.js
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import configurationResource from './ConfigurationResource';
+
+import {suite, test} from 'mocha';
+import {assert} from 'chai';
+
+const CHECKED_CONFIGURATION = {
+    spaces: [{
+        _id: '1space',
+        name: 'Test space'
+    }],
+    clusters: [{
+        _id: '1cluster',
+        space: '1space',
+        name: 'Test cluster',
+        caches: ['1cache'],
+        models: ['1model'],
+        igfss: ['1igfs']
+    }],
+    caches: [{
+        _id: '1cache',
+        space: '1space',
+        name: 'Test cache',
+        clusters: ['1cluster'],
+        models: ['1model']
+    }],
+    domains: [{
+        _id: '1model',
+        space: '1space',
+        name: 'Test model',
+        clusters: ['1cluster'],
+        caches: ['1cache']
+    }],
+    igfss: [{
+        _id: '1igfs',
+        space: '1space',
+        name: 'Test IGFS',
+        clusters: ['1cluster']
+    }]
+};
+
+suite('ConfigurationResourceTestsSuite', () => {
+    test('ConfigurationResourceService correctly populate data', async() => {
+        const service = configurationResource(null);
+        const converted = _.cloneDeep(CHECKED_CONFIGURATION);
+        const res = await service.populate(converted);
+
+        assert.notEqual(res.clusters[0], converted.clusters[0]);
+
+        assert.deepEqual(converted.clusters[0].caches, CHECKED_CONFIGURATION.clusters[0].caches);
+        assert.deepEqual(converted.clusters[0].models, CHECKED_CONFIGURATION.clusters[0].models);
+        assert.deepEqual(converted.clusters[0].igfss, CHECKED_CONFIGURATION.clusters[0].igfss);
+
+        assert.deepEqual(converted.caches[0].clusters, CHECKED_CONFIGURATION.caches[0].clusters);
+        assert.deepEqual(converted.caches[0].models, CHECKED_CONFIGURATION.caches[0].models);
+
+        assert.deepEqual(converted.domains[0].clusters, CHECKED_CONFIGURATION.domains[0].clusters);
+        assert.deepEqual(converted.domains[0].caches, CHECKED_CONFIGURATION.domains[0].caches);
+
+        assert.deepEqual(converted.igfss[0].clusters, CHECKED_CONFIGURATION.igfss[0].clusters);
+    });
+});
diff --git a/modules/frontend/app/configuration/services/ConfigurationResource.ts b/modules/frontend/app/configuration/services/ConfigurationResource.ts
new file mode 100644
index 0000000..f286126
--- /dev/null
+++ b/modules/frontend/app/configuration/services/ConfigurationResource.ts
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+ConfigurationResourceService.$inject = ['$http'];
+
+export default function ConfigurationResourceService($http: ng.IHttpService) {
+    return {
+        read() {
+            return $http.get('/api/v1/configuration/list')
+                .then(({data}) => data)
+                .catch(({data}) => Promise.reject(data));
+        },
+        populate(data) {
+            const {spaces, clusters, caches, igfss, domains} = _.cloneDeep(data);
+
+            _.forEach(clusters, (cluster) => {
+                cluster.caches = _.filter(caches, ({_id}) => _.includes(cluster.caches, _id));
+
+                _.forEach(cluster.caches, (cache) => {
+                    cache.domains = _.filter(domains, ({_id}) => _.includes(cache.domains, _id));
+
+                    if (_.get(cache, 'nodeFilter.kind') === 'IGFS')
+                        cache.nodeFilter.IGFS.instance = _.find(igfss, {_id: cache.nodeFilter.IGFS.igfs});
+                });
+
+                cluster.igfss = _.filter(igfss, ({_id}) => _.includes(cluster.igfss, _id));
+            });
+
+            return Promise.resolve({spaces, clusters, caches, igfss, domains});
+        }
+    };
+}
diff --git a/modules/frontend/app/configuration/services/ConfigureState.ts b/modules/frontend/app/configuration/services/ConfigureState.ts
new file mode 100644
index 0000000..8484818
--- /dev/null
+++ b/modules/frontend/app/configuration/services/ConfigureState.ts
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {BehaviorSubject, Subject} from 'rxjs';
+import {scan, tap} from 'rxjs/operators';
+
+export default class ConfigureState {
+    actions$: Subject<{type: string}>;
+
+    constructor() {
+        this.actions$ = new Subject();
+        this.state$ = new BehaviorSubject({});
+        this._combinedReducer = (state, action) => state;
+
+        const reducer = (state = {}, action) => {
+            try {
+                return this._combinedReducer(state, action);
+            }
+            catch (e) {
+                console.error(e);
+
+                return state;
+            }
+        };
+
+        this.actions$.pipe(
+            scan(reducer, {}),
+            tap((v) => this.state$.next(v))
+        ).subscribe();
+    }
+
+    addReducer(combineFn) {
+        const old = this._combinedReducer;
+
+        this._combinedReducer = (state, action) => combineFn(old(state, action), action);
+
+        return this;
+    }
+
+    dispatchAction(action) {
+        if (typeof action === 'function')
+            return action((a) => this.actions$.next(a), () => this.state$.getValue());
+
+        this.actions$.next(action);
+
+        return action;
+    }
+}
diff --git a/modules/frontend/app/configuration/services/IGFSs.ts b/modules/frontend/app/configuration/services/IGFSs.ts
new file mode 100644
index 0000000..3cfa99e
--- /dev/null
+++ b/modules/frontend/app/configuration/services/IGFSs.ts
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import ObjectID from 'bson-objectid';
+import omit from 'lodash/fp/omit';
+import get from 'lodash/get';
+
+export default class IGFSs {
+    static $inject = ['$http'];
+
+    igfsModes = [
+        {value: 'PRIMARY', label: 'PRIMARY'},
+        {value: 'PROXY', label: 'PROXY'},
+        {value: 'DUAL_SYNC', label: 'DUAL_SYNC'},
+        {value: 'DUAL_ASYNC', label: 'DUAL_ASYNC'}
+    ];
+
+    constructor(private $http: ng.IHttpService) {}
+
+    getIGFS(igfsID: string) {
+        return this.$http.get(`/api/v1/configuration/igfs/${igfsID}`);
+    }
+
+    getBlankIGFS() {
+        return {
+            _id: ObjectID.generate(),
+            ipcEndpointEnabled: true,
+            fragmentizerEnabled: true,
+            colocateMetadata: true,
+            relaxedConsistency: true,
+            secondaryFileSystem: {
+                kind: 'Caching'
+            }
+        };
+    }
+
+    affinnityGroupSize = {
+        default: 512,
+        min: 1
+    };
+
+    defaultMode = {
+        values: [
+            {value: 'PRIMARY', label: 'PRIMARY'},
+            {value: 'PROXY', label: 'PROXY'},
+            {value: 'DUAL_SYNC', label: 'DUAL_SYNC'},
+            {value: 'DUAL_ASYNC', label: 'DUAL_ASYNC'}
+        ],
+        default: 'DUAL_ASYNC'
+    };
+
+    secondaryFileSystemEnabled = {
+        requiredWhenIGFSProxyMode: (igfs) => {
+            if (get(igfs, 'defaultMode') === 'PROXY')
+                return get(igfs, 'secondaryFileSystemEnabled') === true;
+
+            return true;
+        },
+        requiredWhenPathModeProxyMode: (igfs) => {
+            if (get(igfs, 'pathModes', []).some((pm) => pm.mode === 'PROXY'))
+                return get(igfs, 'secondaryFileSystemEnabled') === true;
+
+            return true;
+        }
+    };
+
+    normalize = omit(['__v', 'space', 'clusters']);
+
+    addSecondaryFsNameMapper(igfs) {
+        if (!_.get(igfs, 'secondaryFileSystem.userNameMapper.Chained.mappers'))
+            _.set(igfs, 'secondaryFileSystem.userNameMapper.Chained.mappers', []);
+
+        const item = {_id: ObjectID.generate(), kind: 'Basic'};
+
+        _.get(igfs, 'secondaryFileSystem.userNameMapper.Chained.mappers').push(item);
+
+        return item;
+    }
+}
diff --git a/modules/frontend/app/configuration/services/Models.ts b/modules/frontend/app/configuration/services/Models.ts
new file mode 100644
index 0000000..065bbe5
--- /dev/null
+++ b/modules/frontend/app/configuration/services/Models.ts
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import ObjectID from 'bson-objectid';
+import omit from 'lodash/fp/omit';
+import {DomainModel, Field, Index, IndexField, KeyField, ShortDomainModel, ValueField} from '../types';
+
+export default class Models {
+    static $inject = ['$http'];
+
+    constructor(private $http: ng.IHttpService) {}
+
+    getModel(modelID: string) {
+        return this.$http.get<{data: DomainModel[]}>(`/api/v1/configuration/domains/${modelID}`);
+    }
+
+    getBlankModel(): DomainModel {
+        return {
+            _id: ObjectID.generate(),
+            generatePojo: true,
+            caches: [],
+            queryKeyFields: [],
+            queryMetadata: 'Configuration'
+        };
+    }
+
+    queryMetadata = {
+        values: [
+            {label: 'Annotations', value: 'Annotations'},
+            {label: 'Configuration', value: 'Configuration'}
+        ]
+    };
+
+    indexType = {
+        values: [
+            {label: 'SORTED', value: 'SORTED'},
+            {label: 'FULLTEXT', value: 'FULLTEXT'},
+            {label: 'GEOSPATIAL', value: 'GEOSPATIAL'}
+        ]
+    };
+
+    inlineSizeTypes = [
+        {label: 'Auto', value: -1},
+        {label: 'Custom', value: 1},
+        {label: 'Disabled', value: 0}
+    ];
+
+    inlineSizeType = {
+        _val(queryIndex) {
+            return (queryIndex.inlineSizeType === null || queryIndex.inlineSizeType === void 0) ? -1 : queryIndex.inlineSizeType;
+        },
+        onChange: (queryIndex) => {
+            const inlineSizeType = this.inlineSizeType._val(queryIndex);
+            switch (inlineSizeType) {
+                case 1:
+                    return queryIndex.inlineSize = queryIndex.inlineSize > 0 ? queryIndex.inlineSize : null;
+                case 0:
+                case -1:
+                    return queryIndex.inlineSize = queryIndex.inlineSizeType;
+                default: break;
+            }
+        },
+        default: 'Auto'
+    };
+
+    fieldProperties = {
+        typesWithPrecision: ['BigDecimal', 'String', 'byte[]'],
+        fieldPresentation: (entity, available) => {
+            if (!entity)
+                return '';
+
+            const precision = available('2.7.0') && this.fieldProperties.precisionAvailable(entity);
+            const scale = available('2.7.0') && this.fieldProperties.scaleAvailable(entity);
+
+            return `${entity.name || ''} ${entity.className || ''}${precision && entity.precision ? ' (' + entity.precision : ''}\
+${scale && entity.precision && entity.scale ? ',' + entity.scale : ''}${precision && entity.precision ? ')' : ''}\
+${available('2.3.0') && entity.notNull ? ' Not NULL' : ''}${available('2.4.0') && entity.defaultValue ? ' DEFAULT ' + entity.defaultValue : ''}`;
+        },
+        precisionAvailable: (entity) => entity && this.fieldProperties.typesWithPrecision.includes(entity.className),
+        scaleAvailable: (entity) => entity && entity.className === 'BigDecimal'
+    };
+
+    indexSortDirection = {
+        values: [
+            {value: true, label: 'ASC'},
+            {value: false, label: 'DESC'}
+        ],
+        default: true
+    };
+
+    normalize = omit(['__v', 'space']);
+
+    addIndexField(fields: IndexField[]) {
+        return fields[fields.push({_id: ObjectID.generate(), direction: true}) - 1];
+    }
+
+    addIndex(model: DomainModel) {
+        if (!model)
+            return;
+
+        if (!model.indexes)
+            model.indexes = [];
+
+        model.indexes.push({
+            _id: ObjectID.generate(),
+            name: '',
+            indexType: 'SORTED',
+            fields: []
+        });
+
+        return model.indexes[model.indexes.length - 1];
+    }
+
+    hasIndex(model: DomainModel) {
+        return model.queryMetadata === 'Configuration'
+            ? !!(model.keyFields && model.keyFields.length)
+            : (!model.generatePojo || !model.databaseSchema && !model.databaseTable);
+    }
+
+    toShortModel(model: DomainModel): ShortDomainModel {
+        return {
+            _id: model._id,
+            keyType: model.keyType,
+            valueType: model.valueType,
+            hasIndex: this.hasIndex(model)
+        };
+    }
+
+    queryIndexes = {
+        /**
+         * Validates query indexes for completeness
+         */
+        complete: ($value: Index[] = []) => $value.every((index) => (
+            index.name && index.indexType &&
+            index.fields && index.fields.length && index.fields.every((field) => !!field.name))
+        ),
+        /**
+         * Checks if field names used in indexes exist
+         */
+        fieldsExist: ($value: Index[] = [], fields: Field[] = []) => {
+            const names = new Set(fields.map((field) => field.name));
+            return $value.every((index) => index.fields && index.fields.every((field) => names.has(field.name)));
+        },
+        /**
+         * Check if fields of query indexes have unique names
+         */
+        indexFieldsHaveUniqueNames: ($value: Index[] = []) => {
+            return $value.every((index) => {
+                if (!index.fields)
+                    return true;
+
+                const uniqueNames = new Set(index.fields.map((ec) => ec.name));
+                return uniqueNames.size === index.fields.length;
+            });
+        }
+    };
+
+    /**
+     * Removes instances of removed fields from queryKeyFields and index fields
+     */
+    removeInvalidFields(model: DomainModel): DomainModel {
+        if (!model)
+            return model;
+
+        const fieldNames = new Set((model.fields || []).map((f) => f.name));
+        return {
+            ...model,
+            queryKeyFields: (model.queryKeyFields || []).filter((queryKeyField) => fieldNames.has(queryKeyField)),
+            indexes: (model.indexes || []).map((index) => ({
+                ...index,
+                fields: (index.fields || []).filter((indexField) => fieldNames.has(indexField.name))
+            }))
+        };
+    }
+
+    /**
+     * Checks that collection of DB fields has unique DB and Java field names
+     */
+    storeKeyDBFieldsUnique(DBFields: (KeyField|ValueField)[] = []) {
+        return ['databaseFieldName', 'javaFieldName'].every((key) => {
+            const items = new Set(DBFields.map((field) => field[key]));
+            return items.size === DBFields.length;
+        });
+    }
+}
diff --git a/modules/frontend/app/configuration/services/PageConfigure.ts b/modules/frontend/app/configuration/services/PageConfigure.ts
new file mode 100644
index 0000000..f3f7346
--- /dev/null
+++ b/modules/frontend/app/configuration/services/PageConfigure.ts
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import cloneDeep from 'lodash/cloneDeep';
+
+import {merge, timer} from 'rxjs';
+import {filter, ignoreElements, map, pluck, take, tap} from 'rxjs/operators';
+
+import {ofType} from '../store/effects';
+
+import {default as ConfigureState} from './ConfigureState';
+import {default as ConfigSelectors} from '../store/selectors';
+
+export default class PageConfigure {
+    static $inject = ['ConfigureState', 'ConfigSelectors'];
+
+    constructor(private ConfigureState: ConfigureState, private ConfigSelectors: ConfigSelectors) {}
+
+    getClusterConfiguration({clusterID, isDemo}: {clusterID: string, isDemo: boolean}) {
+        return merge(
+            timer(1).pipe(
+                take(1),
+                tap(() => this.ConfigureState.dispatchAction({type: 'LOAD_COMPLETE_CONFIGURATION', clusterID, isDemo})),
+                ignoreElements()
+            ),
+            this.ConfigureState.actions$.pipe(
+                ofType('LOAD_COMPLETE_CONFIGURATION_ERR'),
+                take(1),
+                pluck('error'),
+                map((e) => Promise.reject(e))
+            ),
+            this.ConfigureState.state$.pipe(
+                this.ConfigSelectors.selectCompleteClusterConfiguration({clusterID, isDemo}),
+                filter((c) => c.__isComplete),
+                take(1),
+                map((data) => ({...data, clusters: [cloneDeep(data.cluster)]}))
+            )
+        ).pipe(take(1))
+        .toPromise();
+    }
+}
diff --git a/modules/frontend/app/configuration/services/SummaryZipper.ts b/modules/frontend/app/configuration/services/SummaryZipper.ts
new file mode 100644
index 0000000..d02f785
--- /dev/null
+++ b/modules/frontend/app/configuration/services/SummaryZipper.ts
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Worker from './summary.worker';
+
+export default function SummaryZipperService($q: ng.IQService) {
+    return function(message) {
+        const defer = $q.defer();
+        const worker = new Worker();
+
+        worker.postMessage(message);
+
+        worker.onmessage = (e) => {
+            defer.resolve(e.data);
+            worker.terminate();
+        };
+
+        worker.onerror = (err) => {
+            defer.reject(err);
+            worker.terminate();
+        };
+
+        return defer.promise;
+    };
+}
+
+SummaryZipperService.$inject = ['$q'];
diff --git a/modules/frontend/app/configuration/services/summary.worker.js b/modules/frontend/app/configuration/services/summary.worker.js
new file mode 100644
index 0000000..bebc675
--- /dev/null
+++ b/modules/frontend/app/configuration/services/summary.worker.js
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import JSZip from 'jszip';
+
+import IgniteMavenGenerator from '../generator/generator/Maven.service';
+import IgniteDockerGenerator from '../generator/generator/Docker.service';
+import IgniteReadmeGenerator from '../generator/generator/Readme.service';
+import IgnitePropertiesGenerator from '../generator/generator/Properties.service';
+import IgniteConfigurationGenerator from '../generator/generator/ConfigurationGenerator';
+
+import IgniteJavaTransformer from '../generator/generator/JavaTransformer.service';
+import IgniteSpringTransformer from '../generator/generator/SpringTransformer.service';
+
+import {nonEmpty, nonNil} from 'app/utils/lodashMixins';
+import get from 'lodash/get';
+import filter from 'lodash/filter';
+import isEmpty from 'lodash/isEmpty';
+
+const maven = new IgniteMavenGenerator();
+const docker = new IgniteDockerGenerator();
+const readme = new IgniteReadmeGenerator();
+const properties = new IgnitePropertiesGenerator();
+
+const java = IgniteJavaTransformer;
+const spring = IgniteSpringTransformer;
+
+const generator = IgniteConfigurationGenerator;
+
+const escapeFileName = (name) => name.replace(/[\\\/*\"\[\],\.:;|=<>?]/g, '-').replace(/ /g, '_');
+
+const kubernetesConfig = (cluster) => {
+    if (!cluster.discovery.Kubernetes)
+        cluster.discovery.Kubernetes = { serviceName: 'ignite' };
+
+    return `apiVersion: v1\n\
+kind: Service\n\
+metadata:\n\
+  # Name of Ignite Service used by Kubernetes IP finder for IP addresses lookup.\n\
+  name: ${ cluster.discovery.Kubernetes.serviceName || 'ignite' }\n\
+spec:\n\
+  clusterIP: None # custom value.\n\
+  ports:\n\
+    - port: 9042 # custom value.\n\
+  selector:\n\
+    # Must be equal to one of the labels set in Ignite pods'\n\
+    # deployement configuration.\n\
+    app: ${ cluster.discovery.Kubernetes.serviceName || 'ignite' }`;
+};
+
+// eslint-disable-next-line no-undef
+onmessage = function(e) {
+    const {cluster, data, demo, targetVer} = e.data;
+
+    const zip = new JSZip();
+
+    if (!data.docker)
+        data.docker = docker.generate(cluster, targetVer);
+
+    zip.file('Dockerfile', data.docker);
+    zip.file('.dockerignore', docker.ignoreFile());
+
+    const cfg = generator.igniteConfiguration(cluster, targetVer, false);
+    const clientCfg = generator.igniteConfiguration(cluster, targetVer, true);
+    const clientNearCaches = filter(cluster.caches, (cache) =>
+        cache.cacheMode === 'PARTITIONED' && get(cache, 'clientNearConfiguration.enabled'));
+
+    const secProps = properties.generate(cfg);
+
+    if (secProps)
+        zip.file('src/main/resources/secret.properties', secProps);
+
+    const srcPath = 'src/main/java';
+    const resourcesPath = 'src/main/resources';
+
+    const serverXml = `${escapeFileName(cluster.name)}-server.xml`;
+    const clientXml = `${escapeFileName(cluster.name)}-client.xml`;
+
+    const metaPath = `${resourcesPath}/META-INF`;
+
+    if (cluster.discovery.kind === 'Kubernetes')
+        zip.file(`${metaPath}/ignite-service.yaml`, kubernetesConfig(cluster));
+
+    zip.file(`${metaPath}/${serverXml}`, spring.igniteConfiguration(cfg, targetVer).asString());
+    zip.file(`${metaPath}/${clientXml}`, spring.igniteConfiguration(clientCfg, targetVer, clientNearCaches).asString());
+
+    const cfgPath = `${srcPath}/config`;
+
+    zip.file(`${cfgPath}/ServerConfigurationFactory.java`, java.igniteConfiguration(cfg, targetVer, 'config', 'ServerConfigurationFactory').asString());
+    zip.file(`${cfgPath}/ClientConfigurationFactory.java`, java.igniteConfiguration(clientCfg, targetVer, 'config', 'ClientConfigurationFactory', clientNearCaches).asString());
+
+    if (java.isDemoConfigured(cluster, demo)) {
+        zip.file(`${srcPath}/demo/DemoStartup.java`, java.nodeStartup(cluster, 'demo.DemoStartup',
+            'ServerConfigurationFactory.createConfiguration()', 'config.ServerConfigurationFactory'));
+    }
+
+    // Generate loader for caches with configured store.
+    const cachesToLoad = filter(cluster.caches, (cache) => nonNil(_.get(cache, 'cacheStoreFactory.kind')));
+
+    if (nonEmpty(cachesToLoad))
+        zip.file(`${srcPath}/load/LoadCaches.java`, java.loadCaches(cachesToLoad, 'load', 'LoadCaches', `"${clientXml}"`));
+
+    const startupPath = `${srcPath}/startup`;
+
+    zip.file(`${startupPath}/ServerNodeSpringStartup.java`, java.nodeStartup(cluster, 'startup.ServerNodeSpringStartup', `"${serverXml}"`));
+    zip.file(`${startupPath}/ClientNodeSpringStartup.java`, java.nodeStartup(cluster, 'startup.ClientNodeSpringStartup', `"${clientXml}"`));
+
+    zip.file(`${startupPath}/ServerNodeCodeStartup.java`, java.nodeStartup(cluster, 'startup.ServerNodeCodeStartup',
+        'ServerConfigurationFactory.createConfiguration()', 'config.ServerConfigurationFactory'));
+    zip.file(`${startupPath}/ClientNodeCodeStartup.java`, java.nodeStartup(cluster, 'startup.ClientNodeCodeStartup',
+        'ClientConfigurationFactory.createConfiguration()', 'config.ClientConfigurationFactory', clientNearCaches));
+
+    zip.file('pom.xml', maven.generate(cluster, targetVer));
+
+    zip.file('README.txt', readme.generate());
+    zip.file('jdbc-drivers/README.txt', readme.generateJDBC());
+
+    if (isEmpty(data.pojos))
+        data.pojos = java.pojos(cluster.caches, true);
+
+    for (const pojo of data.pojos) {
+        if (pojo.keyClass)
+            zip.file(`${srcPath}/${pojo.keyType.replace(/\./g, '/')}.java`, pojo.keyClass);
+
+        zip.file(`${srcPath}/${pojo.valueType.replace(/\./g, '/')}.java`, pojo.valueClass);
+    }
+
+    zip.generateAsync({
+        type: 'blob',
+        compression: 'DEFLATE',
+        mimeType: 'application/octet-stream'
+    }).then((blob) => postMessage(blob));
+};
diff --git a/modules/frontend/app/configuration/states.ts b/modules/frontend/app/configuration/states.ts
new file mode 100644
index 0000000..dad16b3
--- /dev/null
+++ b/modules/frontend/app/configuration/states.ts
@@ -0,0 +1,293 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {StateParams} from '@uirouter/angularjs';
+
+import pageConfigureAdvancedClusterComponent
+    from './components/page-configure-advanced/components/page-configure-advanced-cluster/component';
+import pageConfigureAdvancedModelsComponent
+    from './components/page-configure-advanced/components/page-configure-advanced-models/component';
+import pageConfigureAdvancedCachesComponent
+    from './components/page-configure-advanced/components/page-configure-advanced-caches/component';
+import pageConfigureAdvancedIGFSComponent
+    from './components/page-configure-advanced/components/page-configure-advanced-igfs/component';
+
+import {combineLatest, from} from 'rxjs';
+import {map, switchMap, take} from 'rxjs/operators';
+
+export type ClusterParams = ({clusterID: string} | {clusterID: 'new'}) & StateParams;
+
+const idRegex = `new|[a-z0-9]+`;
+
+const shortCachesResolve = ['ConfigSelectors', 'ConfigureState', 'ConfigEffects', '$transition$', function(ConfigSelectors, ConfigureState, {etp}, $transition$) {
+    if ($transition$.params().clusterID === 'new')
+        return Promise.resolve();
+    return from($transition$.injector().getAsync('_cluster')).pipe(
+        switchMap(() => ConfigureState.state$.pipe(ConfigSelectors.selectCluster($transition$.params().clusterID), take(1))),
+        switchMap((cluster) => {
+            return etp('LOAD_SHORT_CACHES', {ids: cluster.caches, clusterID: cluster._id});
+        })
+    )
+    .toPromise();
+}];
+
+function registerStates($stateProvider) {
+    // Setup the states.
+    $stateProvider
+    .state('base.configuration', {
+        abstract: true,
+        permission: 'configuration',
+        url: '/configuration',
+        onEnter: ['ConfigureState', (ConfigureState) => ConfigureState.dispatchAction({type: 'PRELOAD_STATE', state: {}})],
+        resolve: {
+            _shortClusters: ['ConfigEffects', ({etp}) => {
+                return etp('LOAD_USER_CLUSTERS');
+            }]
+        },
+        resolvePolicy: {
+            async: 'NOWAIT'
+        }
+    })
+    .state('base.configuration.overview', {
+        url: '/overview',
+        component: 'pageConfigureOverview',
+        permission: 'configuration',
+        tfMetaTags: {
+            title: 'Configuration'
+        }
+    })
+    .state('base.configuration.edit', {
+        url: `/{clusterID:${idRegex}}`,
+        permission: 'configuration',
+        component: 'pageConfigure',
+        resolve: {
+            _cluster: ['ConfigEffects', '$transition$', ({etp}, $transition$) => {
+                return $transition$.injector().getAsync('_shortClusters').then(() => {
+                    return etp('LOAD_AND_EDIT_CLUSTER', {clusterID: $transition$.params().clusterID});
+                });
+            }]
+        },
+        data: {
+            errorState: 'base.configuration.overview'
+        },
+        redirectTo: ($transition$) => {
+            const [ConfigureState, ConfigSelectors] = ['ConfigureState', 'ConfigSelectors'].map((t) => $transition$.injector().get(t));
+            const waitFor = ['_cluster', '_shortClusters'].map((t) => $transition$.injector().getAsync(t));
+            return from(Promise.all(waitFor)).pipe(
+                switchMap(() => {
+                    return combineLatest(
+                        ConfigureState.state$.pipe(ConfigSelectors.selectCluster($transition$.params().clusterID), take(1)),
+                        ConfigureState.state$.pipe(ConfigSelectors.selectShortClusters(), take(1))
+                    );
+                }),
+                map(([cluster = {caches: []}, clusters]) => {
+                    return (clusters.value.size > 10 || cluster.caches.length > 5)
+                        ? 'base.configuration.edit.advanced'
+                        : 'base.configuration.edit.basic';
+                })
+            )
+            .toPromise();
+        },
+        failState: 'signin',
+        tfMetaTags: {
+            title: 'Configuration'
+        }
+    })
+    .state('base.configuration.edit.basic', {
+        url: '/basic',
+        component: 'pageConfigureBasic',
+        permission: 'configuration',
+        resolve: {
+            _shortCaches: shortCachesResolve
+        },
+        resolvePolicy: {
+            async: 'NOWAIT'
+        },
+        tfMetaTags: {
+            title: 'Basic Configuration'
+        }
+    })
+    .state('base.configuration.edit.advanced', {
+        url: '/advanced',
+        component: 'pageConfigureAdvanced',
+        permission: 'configuration',
+        redirectTo: 'base.configuration.edit.advanced.cluster'
+    })
+    .state('base.configuration.edit.advanced.cluster', {
+        url: '/cluster',
+        component: pageConfigureAdvancedClusterComponent.name,
+        permission: 'configuration',
+        resolve: {
+            _shortCaches: shortCachesResolve
+        },
+        resolvePolicy: {
+            async: 'NOWAIT'
+        },
+        tfMetaTags: {
+            title: 'Configure Cluster'
+        }
+    })
+    .state('base.configuration.edit.advanced.caches', {
+        url: '/caches',
+        permission: 'configuration',
+        component: pageConfigureAdvancedCachesComponent.name,
+        resolve: {
+            _shortCachesAndModels: ['ConfigSelectors', 'ConfigureState', 'ConfigEffects', '$transition$', (ConfigSelectors, ConfigureState, {etp}, $transition$) => {
+                if ($transition$.params().clusterID === 'new')
+                    return Promise.resolve();
+
+                return from($transition$.injector().getAsync('_cluster')).pipe(
+                    switchMap(() => ConfigureState.state$.pipe(ConfigSelectors.selectCluster($transition$.params().clusterID), take(1))),
+                    map((cluster) => {
+                        return Promise.all([
+                            etp('LOAD_SHORT_CACHES', {ids: cluster.caches, clusterID: cluster._id}),
+                            etp('LOAD_SHORT_MODELS', {ids: cluster.models, clusterID: cluster._id}),
+                            etp('LOAD_SHORT_IGFSS', {ids: cluster.igfss, clusterID: cluster._id})
+                        ]);
+                    })
+                )
+                .toPromise();
+            }]
+        },
+        resolvePolicy: {
+            async: 'NOWAIT'
+        },
+        tfMetaTags: {
+            title: 'Configure Caches'
+        }
+    })
+    .state('base.configuration.edit.advanced.caches.cache', {
+        url: `/{cacheID:${idRegex}}`,
+        permission: 'configuration',
+        resolve: {
+            _cache: ['ConfigEffects', '$transition$', ({etp}, $transition$) => {
+                const {clusterID, cacheID} = $transition$.params();
+
+                if (cacheID === 'new')
+                    return Promise.resolve();
+
+                return etp('LOAD_CACHE', {cacheID});
+            }]
+        },
+        data: {
+            errorState: 'base.configuration.edit.advanced.caches'
+        },
+        resolvePolicy: {
+            async: 'NOWAIT'
+        },
+        tfMetaTags: {
+            title: 'Configure Caches'
+        }
+    })
+    .state('base.configuration.edit.advanced.models', {
+        url: '/models',
+        component: pageConfigureAdvancedModelsComponent.name,
+        permission: 'configuration',
+        resolve: {
+            _shortCachesAndModels: ['ConfigSelectors', 'ConfigureState', 'ConfigEffects', '$transition$', (ConfigSelectors, ConfigureState, {etp}, $transition$) => {
+                if ($transition$.params().clusterID === 'new')
+                    return Promise.resolve();
+
+                return from($transition$.injector().getAsync('_cluster')).pipe(
+                    switchMap(() => ConfigureState.state$.pipe(ConfigSelectors.selectCluster($transition$.params().clusterID), take(1))),
+                    map((cluster) => {
+                        return Promise.all([
+                            etp('LOAD_SHORT_CACHES', {ids: cluster.caches, clusterID: cluster._id}),
+                            etp('LOAD_SHORT_MODELS', {ids: cluster.models, clusterID: cluster._id})
+                        ]);
+                    })
+                ).toPromise();
+            }]
+        },
+        resolvePolicy: {
+            async: 'NOWAIT'
+        },
+        tfMetaTags: {
+            title: 'Configure SQL Schemes'
+        }
+    })
+    .state('base.configuration.edit.advanced.models.model', {
+        url: `/{modelID:${idRegex}}`,
+        resolve: {
+            _cache: ['ConfigEffects', '$transition$', ({etp}, $transition$) => {
+                const {clusterID, modelID} = $transition$.params();
+
+                if (modelID === 'new')
+                    return Promise.resolve();
+
+                return etp('LOAD_MODEL', {modelID});
+            }]
+        },
+        data: {
+            errorState: 'base.configuration.edit.advanced.models'
+        },
+        permission: 'configuration',
+        resolvePolicy: {
+            async: 'NOWAIT'
+        }
+    })
+    .state('base.configuration.edit.advanced.igfs', {
+        url: '/igfs',
+        component: pageConfigureAdvancedIGFSComponent.name,
+        permission: 'configuration',
+        resolve: {
+            _shortIGFSs: ['ConfigSelectors', 'ConfigureState', 'ConfigEffects', '$transition$', (ConfigSelectors, ConfigureState, {etp}, $transition$) => {
+                if ($transition$.params().clusterID === 'new')
+                    return Promise.resolve();
+
+                return from($transition$.injector().getAsync('_cluster')).pipe(
+                    switchMap(() => ConfigureState.state$.pipe(ConfigSelectors.selectCluster($transition$.params().clusterID), take(1))),
+                    map((cluster) => {
+                        return Promise.all([
+                            etp('LOAD_SHORT_IGFSS', {ids: cluster.igfss, clusterID: cluster._id})
+                        ]);
+                    })
+                ).toPromise();
+            }]
+        },
+        resolvePolicy: {
+            async: 'NOWAIT'
+        },
+        tfMetaTags: {
+            title: 'Configure IGFS'
+        }
+    })
+    .state('base.configuration.edit.advanced.igfs.igfs', {
+        url: `/{igfsID:${idRegex}}`,
+        permission: 'configuration',
+        resolve: {
+            _igfs: ['ConfigEffects', '$transition$', ({etp}, $transition$) => {
+                const {clusterID, igfsID} = $transition$.params();
+
+                if (igfsID === 'new')
+                    return Promise.resolve();
+
+                return etp('LOAD_IGFS', {igfsID});
+            }]
+        },
+        data: {
+            errorState: 'base.configuration.edit.advanced.igfs'
+        },
+        resolvePolicy: {
+            async: 'NOWAIT'
+        }
+    });
+}
+
+registerStates.$inject = ['$stateProvider'];
+
+export {registerStates};
diff --git a/modules/frontend/app/configuration/store/actionCreators.js b/modules/frontend/app/configuration/store/actionCreators.js
new file mode 100644
index 0000000..50b8e17
--- /dev/null
+++ b/modules/frontend/app/configuration/store/actionCreators.js
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+    ADVANCED_SAVE_CACHE,
+    ADVANCED_SAVE_CLUSTER,
+    ADVANCED_SAVE_COMPLETE_CONFIGURATION,
+    ADVANCED_SAVE_IGFS,
+    ADVANCED_SAVE_MODEL,
+    BASIC_SAVE,
+    BASIC_SAVE_AND_DOWNLOAD,
+    BASIC_SAVE_ERR,
+    BASIC_SAVE_OK,
+    COMPLETE_CONFIGURATION,
+    CONFIRM_CLUSTERS_REMOVAL,
+    CONFIRM_CLUSTERS_REMOVAL_OK,
+    REMOVE_CLUSTER_ITEMS,
+    REMOVE_CLUSTER_ITEMS_CONFIRMED
+} from './actionTypes';
+
+/**
+ * @typedef {object} IRemoveClusterItemsAction
+ * @prop {'REMOVE_CLUSTER_ITEMS'} type
+ * @prop {('caches'|'igfss'|'models')} itemType
+ * @prop {string} clusterID
+ * @prop {Array<string>} itemIDs
+ * @prop {boolean} save
+ * @prop {boolean} confirm
+ */
+
+/**
+ * @param {string} clusterID
+ * @param {('caches'|'igfss'|'models')} itemType
+ * @param {Array<string>} itemIDs
+ * @param {boolean} [save=false]
+ * @param {boolean} [confirm=true]
+ * @returns {IRemoveClusterItemsAction}
+ */
+export const removeClusterItems = (clusterID, itemType, itemIDs, save = false, confirm = true) => ({
+    type: REMOVE_CLUSTER_ITEMS,
+    itemType,
+    clusterID,
+    itemIDs,
+    save,
+    confirm
+});
+
+/**
+ * @typedef {object} IRemoveClusterItemsConfirmed
+ * @prop {string} clusterID
+ * @prop {'REMOVE_CLUSTER_ITEMS_CONFIRMED'} type
+ * @prop {('caches'|'igfss'|'models')} itemType
+ * @prop {Array<string>} itemIDs
+ */
+
+/**
+ * @param {string} clusterID
+ * @param {(('caches'|'igfss'|'models'))} itemType
+ * @param {Array<string>} itemIDs
+ * @returns {IRemoveClusterItemsConfirmed}
+ */
+export const removeClusterItemsConfirmed = (clusterID, itemType, itemIDs) => ({
+    type: REMOVE_CLUSTER_ITEMS_CONFIRMED,
+    itemType,
+    clusterID,
+    itemIDs
+});
+
+const applyChangedIDs = (edit) => ({
+    cluster: {
+        ...edit.changes.cluster,
+        caches: edit.changes.caches.ids,
+        igfss: edit.changes.igfss.ids,
+        models: edit.changes.models.ids
+    },
+    caches: edit.changes.caches.changedItems,
+    igfss: edit.changes.igfss.changedItems,
+    models: edit.changes.models.changedItems
+});
+
+const upsertCluster = (cluster) => ({
+    type: 'UPSERT_CLUSTER',
+    cluster
+});
+
+export const changeItem = (type, item) => ({
+    type: 'UPSERT_CLUSTER_ITEM',
+    itemType: type,
+    item
+});
+
+/**
+ * @typedef {object} IAdvancedSaveCompleteConfigurationAction
+ * @prop {'ADVANCED_SAVE_COMPLETE_CONFIGURATION'} type
+ * @prop {object} changedItems
+ * @prop {Array<object>} [prevActions]
+ */
+
+/**
+ * @returns {IAdvancedSaveCompleteConfigurationAction}
+ */
+// TODO: add support for prev actions
+export const advancedSaveCompleteConfiguration = (edit) => {
+    return {
+        type: ADVANCED_SAVE_COMPLETE_CONFIGURATION,
+        changedItems: applyChangedIDs(edit)
+    };
+};
+
+/**
+ * @typedef {object} IConfirmClustersRemovalAction
+ * @prop {'CONFIRM_CLUSTERS_REMOVAL'} type
+ * @prop {Array<string>} clusterIDs
+ */
+
+/**
+ * @param {Array<string>} clusterIDs
+ * @returns {IConfirmClustersRemovalAction}
+ */
+export const confirmClustersRemoval = (clusterIDs) => ({
+    type: CONFIRM_CLUSTERS_REMOVAL,
+    clusterIDs
+});
+
+/**
+ * @typedef {object} IConfirmClustersRemovalActionOK
+ * @prop {'CONFIRM_CLUSTERS_REMOVAL_OK'} type
+ */
+
+/**
+ * @returns {IConfirmClustersRemovalActionOK}
+ */
+export const confirmClustersRemovalOK = () => ({
+    type: CONFIRM_CLUSTERS_REMOVAL_OK
+});
+
+export const completeConfiguration = (configuration) => ({
+    type: COMPLETE_CONFIGURATION,
+    configuration
+});
+
+export const advancedSaveCluster = (cluster, download = false) => ({type: ADVANCED_SAVE_CLUSTER, cluster, download});
+export const advancedSaveCache = (cache, download = false) => ({type: ADVANCED_SAVE_CACHE, cache, download});
+export const advancedSaveIGFS = (igfs, download = false) => ({type: ADVANCED_SAVE_IGFS, igfs, download});
+export const advancedSaveModel = (model, download = false) => ({type: ADVANCED_SAVE_MODEL, model, download});
+
+export const basicSave = (cluster) => ({type: BASIC_SAVE, cluster});
+export const basicSaveAndDownload = (cluster) => ({type: BASIC_SAVE_AND_DOWNLOAD, cluster});
+export const basicSaveOK = (changedItems) => ({type: BASIC_SAVE_OK, changedItems});
+export const basicSaveErr = (changedItems, res) => ({
+    type: BASIC_SAVE_ERR,
+    changedItems,
+    error: {
+        message: `Failed to save cluster "${changedItems.cluster.name}": ${res.data}.`
+    }
+});
diff --git a/modules/frontend/app/configuration/store/actionTypes.js b/modules/frontend/app/configuration/store/actionTypes.js
new file mode 100644
index 0000000..aa8a4f3
--- /dev/null
+++ b/modules/frontend/app/configuration/store/actionTypes.js
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const CONFIRM_CLUSTERS_REMOVAL = 'CONFIRM_CLUSTERS_REMOVAL';
+export const CONFIRM_CLUSTERS_REMOVAL_OK = 'CONFIRM_CLUSTERS_REMOVAL_OK';
+export const REMOVE_CLUSTER_ITEMS = 'REMOVE_CLUSTER_ITEMS';
+export const REMOVE_CLUSTER_ITEMS_CONFIRMED = 'REMOVE_CLUSTER_ITEMS_CONFIRMED';
+export const ADVANCED_SAVE_COMPLETE_CONFIGURATION = 'ADVANCED_SAVE_COMPLETE_CONFIGURATION';
+export const COMPLETE_CONFIGURATION = 'COMPLETE_CONFIGURATION';
+export const ADVANCED_SAVE_CLUSTER = 'ADVANCED_SAVE_CLUSTER';
+export const ADVANCED_SAVE_CACHE = 'ADVANCED_SAVE_CACHE';
+export const ADVANCED_SAVE_IGFS = 'ADVANCED_SAVE_IGFS';
+export const ADVANCED_SAVE_MODEL = 'ADVANCED_SAVE_MODEL';
+export const BASIC_SAVE = 'BASIC_SAVE';
+export const BASIC_SAVE_AND_DOWNLOAD = 'BASIC_SAVE_AND_DOWNLOAD';
+export const BASIC_SAVE_OK = 'BASIC_SAVE_OK';
+export const BASIC_SAVE_ERR = 'BASIC_SAVE_ERR';
diff --git a/modules/frontend/app/configuration/store/effects.js b/modules/frontend/app/configuration/store/effects.js
new file mode 100644
index 0000000..4d65035
--- /dev/null
+++ b/modules/frontend/app/configuration/store/effects.js
@@ -0,0 +1,776 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {empty, from, merge, of} from 'rxjs';
+import {
+    catchError,
+    exhaustMap,
+    filter,
+    ignoreElements,
+    map,
+    mapTo,
+    pluck,
+    switchMap,
+    take,
+    tap,
+    withLatestFrom,
+    zip
+} from 'rxjs/operators';
+import uniq from 'lodash/uniq';
+import {uniqueName} from 'app/utils/uniqueName';
+import {defaultNames} from '../defaultNames';
+
+import {
+    cachesActionTypes,
+    clustersActionTypes,
+    igfssActionTypes,
+    modelsActionTypes,
+    shortCachesActionTypes,
+    shortClustersActionTypes,
+    shortIGFSsActionTypes,
+    shortModelsActionTypes
+} from './reducer';
+
+import {
+    ADVANCED_SAVE_CACHE,
+    ADVANCED_SAVE_CLUSTER,
+    ADVANCED_SAVE_COMPLETE_CONFIGURATION,
+    ADVANCED_SAVE_IGFS,
+    ADVANCED_SAVE_MODEL,
+    BASIC_SAVE,
+    BASIC_SAVE_AND_DOWNLOAD,
+    BASIC_SAVE_OK,
+    COMPLETE_CONFIGURATION,
+    CONFIRM_CLUSTERS_REMOVAL,
+    CONFIRM_CLUSTERS_REMOVAL_OK,
+    REMOVE_CLUSTER_ITEMS,
+    REMOVE_CLUSTER_ITEMS_CONFIRMED
+} from './actionTypes';
+
+import {
+    advancedSaveCompleteConfiguration,
+    basicSaveErr,
+    basicSaveOK,
+    completeConfiguration,
+    confirmClustersRemovalOK,
+    removeClusterItemsConfirmed
+} from './actionCreators';
+
+import ConfigureState from '../services/ConfigureState';
+import ConfigurationDownload from '../services/ConfigurationDownload';
+import ConfigSelectors from './selectors';
+import Clusters from '../services/Clusters';
+import Caches from '../services/Caches';
+import Models from '../services/Models';
+import IGFSs from '../services/IGFSs';
+import {Confirm} from 'app/services/Confirm.service';
+
+export const ofType = (type) => (s) => s.pipe(filter((a) => a.type === type));
+
+export default class ConfigEffects {
+    static $inject = [
+        'ConfigureState',
+        'Caches',
+        'IGFSs',
+        'Models',
+        'ConfigSelectors',
+        'Clusters',
+        '$state',
+        'IgniteMessages',
+        'IgniteConfirm',
+        'Confirm',
+        'ConfigurationDownload'
+    ];
+
+    /**
+     * @param {ConfigureState} ConfigureState
+     * @param {Caches} Caches
+     * @param {IGFSs} IGFSs
+     * @param {Models} Models
+     * @param {ConfigSelectors} ConfigSelectors
+     * @param {Clusters} Clusters
+     * @param {object} $state
+     * @param {object} IgniteMessages
+     * @param {object} IgniteConfirm
+     * @param {Confirm} Confirm
+     * @param {ConfigurationDownload} ConfigurationDownload
+     */
+    constructor(ConfigureState, Caches, IGFSs, Models, ConfigSelectors, Clusters, $state, IgniteMessages, IgniteConfirm, Confirm, ConfigurationDownload) {
+        this.ConfigureState = ConfigureState;
+        this.ConfigSelectors = ConfigSelectors;
+        this.IGFSs = IGFSs;
+        this.Models = Models;
+        this.Caches = Caches;
+        this.Clusters = Clusters;
+        this.$state = $state;
+        this.IgniteMessages = IgniteMessages;
+        this.IgniteConfirm = IgniteConfirm;
+        this.Confirm = Confirm;
+        this.configurationDownload = ConfigurationDownload;
+
+        this.loadConfigurationEffect$ = this.ConfigureState.actions$.pipe(
+            ofType('LOAD_COMPLETE_CONFIGURATION'),
+            exhaustMap((action) => {
+                return from(this.Clusters.getConfiguration(action.clusterID)).pipe(
+                    switchMap(({data}) => of(
+                        completeConfiguration(data),
+                        {type: 'LOAD_COMPLETE_CONFIGURATION_OK', data}
+                    )),
+                    catchError((error) => of({
+                        type: 'LOAD_COMPLETE_CONFIGURATION_ERR',
+                        error: {
+                            message: `Failed to load cluster configuration: ${error.data}.`
+                        },
+                        action
+                    })));
+            })
+        );
+
+        this.storeConfigurationEffect$ = this.ConfigureState.actions$.pipe(
+            ofType(COMPLETE_CONFIGURATION),
+            exhaustMap(({configuration: {cluster, caches, models, igfss}}) => of(...[
+                cluster && {type: clustersActionTypes.UPSERT, items: [cluster]},
+                caches && caches.length && {type: cachesActionTypes.UPSERT, items: caches},
+                models && models.length && {type: modelsActionTypes.UPSERT, items: models},
+                igfss && igfss.length && {type: igfssActionTypes.UPSERT, items: igfss}
+            ].filter((v) => v)))
+        );
+
+        this.saveCompleteConfigurationEffect$ = this.ConfigureState.actions$.pipe(
+            ofType(ADVANCED_SAVE_COMPLETE_CONFIGURATION),
+            switchMap((action) => {
+                const actions = [
+                    {
+                        type: modelsActionTypes.UPSERT,
+                        items: action.changedItems.models
+                    },
+                    {
+                        type: shortModelsActionTypes.UPSERT,
+                        items: action.changedItems.models.map((m) => this.Models.toShortModel(m))
+                    },
+                    {
+                        type: igfssActionTypes.UPSERT,
+                        items: action.changedItems.igfss
+                    },
+                    {
+                        type: shortIGFSsActionTypes.UPSERT,
+                        items: action.changedItems.igfss
+                    },
+                    {
+                        type: cachesActionTypes.UPSERT,
+                        items: action.changedItems.caches
+                    },
+                    {
+                        type: shortCachesActionTypes.UPSERT,
+                        items: action.changedItems.caches.map(Caches.toShortCache)
+                    },
+                    {
+                        type: clustersActionTypes.UPSERT,
+                        items: [action.changedItems.cluster]
+                    },
+                    {
+                        type: shortClustersActionTypes.UPSERT,
+                        items: [Clusters.toShortCluster(action.changedItems.cluster)]
+                    }
+                ].filter((a) => a.items.length);
+
+                return merge(
+                    of(...actions),
+                    from(Clusters.saveAdvanced(action.changedItems)).pipe(
+                        switchMap((res) => {
+                            return of(
+                                {type: 'EDIT_CLUSTER', cluster: action.changedItems.cluster},
+                                {type: 'ADVANCED_SAVE_COMPLETE_CONFIGURATION_OK', changedItems: action.changedItems}
+                            );
+                        }),
+                        catchError((res) => {
+                            return of({
+                                type: 'ADVANCED_SAVE_COMPLETE_CONFIGURATION_ERR',
+                                changedItems: action.changedItems,
+                                action,
+                                error: {
+                                    message: `Failed to save cluster "${action.changedItems.cluster.name}": ${res.data}.`
+                                }
+                            }, {
+                                type: 'UNDO_ACTIONS',
+                                actions
+                            });
+                        })
+                    )
+                );
+            })
+        );
+
+        this.addCacheToEditEffect$ = this.ConfigureState.actions$.pipe(
+            ofType('ADD_CACHE_TO_EDIT'),
+            switchMap(() => this.ConfigureState.state$.pipe(this.ConfigSelectors.selectCacheToEdit('new'), take(1))),
+            map((cache) => ({type: 'UPSERT_CLUSTER_ITEM', itemType: 'caches', item: cache}))
+        );
+
+        this.errorNotificationsEffect$ = this.ConfigureState.actions$.pipe(
+            filter((a) => a.error),
+            tap((action) => this.IgniteMessages.showError(action.error)),
+            ignoreElements()
+        );
+
+        this.loadUserClustersEffect$ = this.ConfigureState.actions$.pipe(
+            ofType('LOAD_USER_CLUSTERS'),
+            exhaustMap((a) => {
+                return from(this.Clusters.getClustersOverview()).pipe(
+                    switchMap(({data}) => of(
+                        {type: shortClustersActionTypes.SET, items: data},
+                        {type: `${a.type}_OK`}
+                    )),
+                    catchError((error) => of({
+                        type: `${a.type}_ERR`,
+                        error: {
+                            message: `Failed to load clusters: ${error.data}`
+                        },
+                        action: a
+                    }))
+                );
+            })
+        );
+
+        this.loadAndEditClusterEffect$ = ConfigureState.actions$.pipe(
+            ofType('LOAD_AND_EDIT_CLUSTER'),
+            withLatestFrom(this.ConfigureState.state$.pipe(this.ConfigSelectors.selectShortClustersValue())),
+            exhaustMap(([a, shortClusters]) => {
+                if (a.clusterID === 'new') {
+                    return of(
+                        {
+                            type: 'EDIT_CLUSTER',
+                            cluster: {
+                                ...this.Clusters.getBlankCluster(),
+                                name: uniqueName(defaultNames.cluster, shortClusters)
+                            }
+                        },
+                        {type: 'LOAD_AND_EDIT_CLUSTER_OK'}
+                    );
+                }
+                return this.ConfigureState.state$.pipe(
+                    this.ConfigSelectors.selectCluster(a.clusterID),
+                    take(1),
+                    switchMap((cluster) => {
+                        if (cluster) {
+                            return of(
+                                {type: 'EDIT_CLUSTER', cluster},
+                                {type: 'LOAD_AND_EDIT_CLUSTER_OK'}
+                            );
+                        }
+                        return from(this.Clusters.getCluster(a.clusterID)).pipe(
+                            switchMap(({data}) => of(
+                                {type: clustersActionTypes.UPSERT, items: [data]},
+                                {type: 'EDIT_CLUSTER', cluster: data},
+                                {type: 'LOAD_AND_EDIT_CLUSTER_OK'}
+                            )),
+                            catchError((error) => of({
+                                type: 'LOAD_AND_EDIT_CLUSTER_ERR',
+                                error: {
+                                    message: `Failed to load cluster: ${error.data}.`
+                                }
+                            }))
+                        );
+                    })
+                );
+            })
+        );
+
+        this.loadCacheEffect$ = this.ConfigureState.actions$.pipe(
+            ofType('LOAD_CACHE'),
+            exhaustMap((a) => {
+                return this.ConfigureState.state$.pipe(
+                    this.ConfigSelectors.selectCache(a.cacheID),
+                    take(1),
+                    switchMap((cache) => {
+                        if (cache)
+                            return of({type: `${a.type}_OK`, cache});
+
+                        return from(this.Caches.getCache(a.cacheID)).pipe(
+                            switchMap(({data}) => of(
+                                {type: 'CACHE', cache: data},
+                                {type: `${a.type}_OK`, cache: data}
+                            ))
+                        );
+                    }),
+                    catchError((error) => of({
+                        type: `${a.type}_ERR`,
+                        error: {
+                            message: `Failed to load cache: ${error.data}.`
+                        }
+                    }))
+                );
+            })
+        );
+
+        this.storeCacheEffect$ = this.ConfigureState.actions$.pipe(
+            ofType('CACHE'),
+            map((a) => ({type: cachesActionTypes.UPSERT, items: [a.cache]}))
+        );
+
+        this.loadShortCachesEffect$ = ConfigureState.actions$.pipe(
+            ofType('LOAD_SHORT_CACHES'),
+            exhaustMap((a) => {
+                if (!(a.ids || []).length)
+                    return of({type: `${a.type}_OK`});
+
+                return this.ConfigureState.state$.pipe(
+                    this.ConfigSelectors.selectShortCaches(),
+                    take(1),
+                    switchMap((items) => {
+                        if (!items.pristine && a.ids && a.ids.every((_id) => items.value.has(_id)))
+                            return of({type: `${a.type}_OK`});
+
+                        return from(this.Clusters.getClusterCaches(a.clusterID)).pipe(
+                            switchMap(({data}) => of(
+                                {type: shortCachesActionTypes.UPSERT, items: data},
+                                {type: `${a.type}_OK`}
+                            ))
+                        );
+                    }),
+                    catchError((error) => of({
+                        type: `${a.type}_ERR`,
+                        error: {
+                            message: `Failed to load caches: ${error.data}.`
+                        },
+                        action: a
+                    }))
+                );
+            })
+        );
+
+        this.loadIgfsEffect$ = this.ConfigureState.actions$.pipe(
+            ofType('LOAD_IGFS'),
+            exhaustMap((a) => {
+                return this.ConfigureState.state$.pipe(
+                    this.ConfigSelectors.selectIGFS(a.igfsID),
+                    take(1),
+                    switchMap((igfs) => {
+                        if (igfs)
+                            return of({type: `${a.type}_OK`, igfs});
+
+                        return from(this.IGFSs.getIGFS(a.igfsID)).pipe(
+                            switchMap(({data}) => of(
+                                {type: 'IGFS', igfs: data},
+                                {type: `${a.type}_OK`, igfs: data}
+                            ))
+                        );
+                    }),
+                    catchError((error) => of({
+                        type: `${a.type}_ERR`,
+                        error: {
+                            message: `Failed to load IGFS: ${error.data}.`
+                        }
+                    }))
+                );
+            })
+        );
+
+        this.storeIgfsEffect$ = this.ConfigureState.actions$.pipe(
+            ofType('IGFS'),
+            map((a) => ({type: igfssActionTypes.UPSERT, items: [a.igfs]}))
+        );
+
+        this.loadShortIgfssEffect$ = ConfigureState.actions$.pipe(
+            ofType('LOAD_SHORT_IGFSS'),
+            exhaustMap((a) => {
+                if (!(a.ids || []).length) {
+                    return of(
+                        {type: shortIGFSsActionTypes.UPSERT, items: []},
+                        {type: `${a.type}_OK`}
+                    );
+                }
+                return this.ConfigureState.state$.pipe(
+                    this.ConfigSelectors.selectShortIGFSs(),
+                    take(1),
+                    switchMap((items) => {
+                        if (!items.pristine && a.ids && a.ids.every((_id) => items.value.has(_id)))
+                            return of({type: `${a.type}_OK`});
+
+                        return from(this.Clusters.getClusterIGFSs(a.clusterID)).pipe(
+                            switchMap(({data}) => of(
+                                {type: shortIGFSsActionTypes.UPSERT, items: data},
+                                {type: `${a.type}_OK`}
+                            ))
+                        );
+                    }),
+                    catchError((error) => of({
+                        type: `${a.type}_ERR`,
+                        error: {
+                            message: `Failed to load IGFSs: ${error.data}.`
+                        },
+                        action: a
+                    }))
+                );
+            })
+        );
+
+        this.loadModelEffect$ = this.ConfigureState.actions$.pipe(
+            ofType('LOAD_MODEL'),
+            exhaustMap((a) => {
+                return this.ConfigureState.state$.pipe(
+                    this.ConfigSelectors.selectModel(a.modelID),
+                    take(1),
+                    switchMap((model) => {
+                        if (model)
+                            return of({type: `${a.type}_OK`, model});
+
+                        return from(this.Models.getModel(a.modelID)).pipe(
+                            switchMap(({data}) => of(
+                                {type: 'MODEL', model: data},
+                                {type: `${a.type}_OK`, model: data}
+                            ))
+                        );
+                    }),
+                    catchError((error) => of({
+                        type: `${a.type}_ERR`,
+                        error: {
+                            message: `Failed to load domain model: ${error.data}.`
+                        }
+                    }))
+                );
+            })
+        );
+
+        this.storeModelEffect$ = this.ConfigureState.actions$.pipe(
+            ofType('MODEL'),
+            map((a) => ({type: modelsActionTypes.UPSERT, items: [a.model]}))
+        );
+
+        this.loadShortModelsEffect$ = this.ConfigureState.actions$.pipe(
+            ofType('LOAD_SHORT_MODELS'),
+            exhaustMap((a) => {
+                if (!(a.ids || []).length) {
+                    return of(
+                        {type: shortModelsActionTypes.UPSERT, items: []},
+                        {type: `${a.type}_OK`}
+                    );
+                }
+                return this.ConfigureState.state$.pipe(
+                    this.ConfigSelectors.selectShortModels(),
+                    take(1),
+                    switchMap((items) => {
+                        if (!items.pristine && a.ids && a.ids.every((_id) => items.value.has(_id)))
+                            return of({type: `${a.type}_OK`});
+
+                        return from(this.Clusters.getClusterModels(a.clusterID)).pipe(
+                            switchMap(({data}) => of(
+                                {type: shortModelsActionTypes.UPSERT, items: data},
+                                {type: `${a.type}_OK`}
+                            ))
+                        );
+                    }),
+                    catchError((error) => of({
+                        type: `${a.type}_ERR`,
+                        error: {
+                            message: `Failed to load domain models: ${error.data}.`
+                        },
+                        action: a
+                    }))
+                );
+            })
+        );
+
+        this.basicSaveRedirectEffect$ = this.ConfigureState.actions$.pipe(
+            ofType(BASIC_SAVE_OK),
+            tap((a) => this.$state.go('base.configuration.edit.basic', {clusterID: a.changedItems.cluster._id}, {location: 'replace', custom: {justIDUpdate: true}})),
+            ignoreElements()
+        );
+
+        this.basicDownloadAfterSaveEffect$ = this.ConfigureState.actions$.pipe(
+            ofType(BASIC_SAVE_AND_DOWNLOAD),
+            zip(this.ConfigureState.actions$.pipe(ofType(BASIC_SAVE_OK))),
+            pluck('1'),
+            tap((a) => this.configurationDownload.downloadClusterConfiguration(a.changedItems.cluster)),
+            ignoreElements()
+        );
+
+        this.advancedDownloadAfterSaveEffect$ = merge(
+            this.ConfigureState.actions$.pipe(ofType(ADVANCED_SAVE_CLUSTER)),
+            this.ConfigureState.actions$.pipe(ofType(ADVANCED_SAVE_CACHE)),
+            this.ConfigureState.actions$.pipe(ofType(ADVANCED_SAVE_IGFS)),
+            this.ConfigureState.actions$.pipe(ofType(ADVANCED_SAVE_MODEL)),
+        ).pipe(
+            filter((a) => a.download),
+            zip(this.ConfigureState.actions$.pipe(ofType('ADVANCED_SAVE_COMPLETE_CONFIGURATION_OK'))),
+            pluck('1'),
+            tap((a) => this.configurationDownload.downloadClusterConfiguration(a.changedItems.cluster)),
+            ignoreElements()
+        );
+
+        this.advancedSaveRedirectEffect$ = this.ConfigureState.actions$.pipe(
+            ofType('ADVANCED_SAVE_COMPLETE_CONFIGURATION_OK'),
+            withLatestFrom(this.ConfigureState.actions$.pipe(ofType(ADVANCED_SAVE_COMPLETE_CONFIGURATION))),
+            pluck('1', 'changedItems'),
+            map((req) => {
+                const firstChangedItem = Object.keys(req).filter((k) => k !== 'cluster')
+                    .map((k) => Array.isArray(req[k]) ? [k, req[k][0]] : [k, req[k]])
+                    .filter((v) => v[1])
+                    .pop();
+                return firstChangedItem ? [...firstChangedItem, req.cluster] : ['cluster', req.cluster, req.cluster];
+            }),
+            tap(([type, value, cluster]) => {
+                const go = (state, params = {}) => this.$state.go(
+                    state, {...params, clusterID: cluster._id}, {location: 'replace', custom: {justIDUpdate: true}}
+                );
+
+                switch (type) {
+                    case 'models': {
+                        const state = 'base.configuration.edit.advanced.models.model';
+                        this.IgniteMessages.showInfo(`Model "${value.valueType}" saved`);
+
+                        if (this.$state.is(state) && this.$state.params.modelID !== value._id)
+                            return go(state, {modelID: value._id});
+
+                        break;
+                    }
+
+                    case 'caches': {
+                        const state = 'base.configuration.edit.advanced.caches.cache';
+                        this.IgniteMessages.showInfo(`Cache "${value.name}" saved`);
+
+                        if (this.$state.is(state) && this.$state.params.cacheID !== value._id)
+                            return go(state, {cacheID: value._id});
+
+                        break;
+                    }
+
+                    case 'igfss': {
+                        const state = 'base.configuration.edit.advanced.igfs.igfs';
+                        this.IgniteMessages.showInfo(`IGFS "${value.name}" saved`);
+
+                        if (this.$state.is(state) && this.$state.params.igfsID !== value._id)
+                            return go(state, {igfsID: value._id});
+
+                        break;
+                    }
+
+                    case 'cluster': {
+                        const state = 'base.configuration.edit.advanced.cluster';
+                        this.IgniteMessages.showInfo(`Cluster "${value.name}" saved`);
+
+                        if (this.$state.is(state) && this.$state.params.clusterID !== value._id)
+                            return go(state);
+
+                        break;
+                    }
+
+                    default: break;
+                }
+            }),
+            ignoreElements()
+        );
+
+        this.removeClusterItemsEffect$ = this.ConfigureState.actions$.pipe(
+            ofType(REMOVE_CLUSTER_ITEMS),
+            exhaustMap((a) => {
+                return a.confirm
+                    // TODO: list items to remove in confirmation
+                    ? from(this.Confirm.confirm('Are you sure you want to remove these items?')).pipe(
+                        mapTo(a),
+                        catchError(() => empty())
+                    )
+                    : of(a);
+            }),
+            map((a) => removeClusterItemsConfirmed(a.clusterID, a.itemType, a.itemIDs))
+        );
+
+        this.persistRemovedClusterItemsEffect$ = this.ConfigureState.actions$.pipe(
+            ofType(REMOVE_CLUSTER_ITEMS_CONFIRMED),
+            withLatestFrom(this.ConfigureState.actions$.pipe(ofType(REMOVE_CLUSTER_ITEMS))),
+            filter(([a, b]) => {
+                return a.itemType === b.itemType
+                    && b.save
+                    && JSON.stringify(a.itemIDs) === JSON.stringify(b.itemIDs);
+            }),
+            pluck('0'),
+            withLatestFrom(this.ConfigureState.state$.pipe(pluck('edit'))),
+            map(([action, edit]) => advancedSaveCompleteConfiguration(edit))
+        );
+
+        this.confirmClustersRemovalEffect$ = this.ConfigureState.actions$.pipe(
+            ofType(CONFIRM_CLUSTERS_REMOVAL),
+            pluck('clusterIDs'),
+            switchMap((ids) => this.ConfigureState.state$.pipe(
+                this.ConfigSelectors.selectClusterNames(ids),
+                take(1)
+            )),
+            exhaustMap((names) => {
+                return from(this.Confirm.confirm(`
+                    <p>Are you sure you want to remove these clusters?</p>
+                    <ul>${names.map((name) => `<li>${name}</li>`).join('')}</ul>
+                `)).pipe(
+                    map(confirmClustersRemovalOK),
+                    catchError(() => empty())
+                );
+            })
+        );
+
+        this.persistRemovedClustersLocallyEffect$ = this.ConfigureState.actions$.pipe(
+            ofType(CONFIRM_CLUSTERS_REMOVAL_OK),
+            withLatestFrom(this.ConfigureState.actions$.pipe(ofType(CONFIRM_CLUSTERS_REMOVAL))),
+            switchMap(([, {clusterIDs}]) => of(
+                {type: shortClustersActionTypes.REMOVE, ids: clusterIDs},
+                {type: clustersActionTypes.REMOVE, ids: clusterIDs}
+            ))
+        );
+
+        this.persistRemovedClustersRemotelyEffect$ = this.ConfigureState.actions$.pipe(
+            ofType(CONFIRM_CLUSTERS_REMOVAL_OK),
+            withLatestFrom(
+                this.ConfigureState.actions$.pipe(ofType(CONFIRM_CLUSTERS_REMOVAL)),
+                this.ConfigureState.actions$.pipe(ofType(shortClustersActionTypes.REMOVE)),
+                this.ConfigureState.actions$.pipe(ofType(clustersActionTypes.REMOVE))
+            ),
+            switchMap(([, {clusterIDs}, ...backup]) => this.Clusters.removeCluster$(clusterIDs).pipe(
+                mapTo({
+                    type: 'REMOVE_CLUSTERS_OK'
+                }),
+                catchError((e) => of(
+                    {
+                        type: 'REMOVE_CLUSTERS_ERR',
+                        error: {
+                            message: `Failed to remove clusters: ${e.data}`
+                        }
+                    },
+                    {
+                        type: 'UNDO_ACTIONS',
+                        actions: backup
+                    }
+                ))
+            ))
+        );
+
+        this.notifyRemoteClustersRemoveSuccessEffect$ = this.ConfigureState.actions$.pipe(
+            ofType('REMOVE_CLUSTERS_OK'),
+            withLatestFrom(this.ConfigureState.actions$.pipe(ofType(CONFIRM_CLUSTERS_REMOVAL))),
+            tap(([, {clusterIDs}]) => this.IgniteMessages.showInfo(`Cluster(s) removed: ${clusterIDs.length}`)),
+            ignoreElements()
+        );
+
+        const _applyChangedIDs = (edit, {cache, igfs, model, cluster} = {}) => ({
+            cluster: {
+                ...edit.changes.cluster,
+                ...(cluster ? cluster : {}),
+                caches: cache ? uniq([...edit.changes.caches.ids, cache._id]) : edit.changes.caches.ids,
+                igfss: igfs ? uniq([...edit.changes.igfss.ids, igfs._id]) : edit.changes.igfss.ids,
+                models: model ? uniq([...edit.changes.models.ids, model._id]) : edit.changes.models.ids
+            },
+            caches: cache ? uniq([...edit.changes.caches.changedItems, cache]) : edit.changes.caches.changedItems,
+            igfss: igfs ? uniq([...edit.changes.igfss.changedItems, igfs]) : edit.changes.igfss.changedItems,
+            models: model ? uniq([...edit.changes.models.changedItems, model]) : edit.changes.models.changedItems
+        });
+
+        this.advancedSaveCacheEffect$ = merge(
+            this.ConfigureState.actions$.pipe(ofType(ADVANCED_SAVE_CLUSTER)),
+            this.ConfigureState.actions$.pipe(ofType(ADVANCED_SAVE_CACHE)),
+            this.ConfigureState.actions$.pipe(ofType(ADVANCED_SAVE_IGFS)),
+            this.ConfigureState.actions$.pipe(ofType(ADVANCED_SAVE_MODEL)),
+        ).pipe(
+            withLatestFrom(this.ConfigureState.state$.pipe(pluck('edit'))),
+            map(([action, edit]) => ({
+                type: ADVANCED_SAVE_COMPLETE_CONFIGURATION,
+                changedItems: _applyChangedIDs(edit, action)
+            }))
+        );
+
+        this.basicSaveEffect$ = merge(
+            this.ConfigureState.actions$.pipe(ofType(BASIC_SAVE)),
+            this.ConfigureState.actions$.pipe(ofType(BASIC_SAVE_AND_DOWNLOAD))
+        ).pipe(
+            withLatestFrom(this.ConfigureState.state$.pipe(pluck('edit'))),
+            switchMap(([action, edit]) => {
+                const changedItems = _applyChangedIDs(edit, {cluster: action.cluster});
+                const actions = [{
+                    type: cachesActionTypes.UPSERT,
+                    items: changedItems.caches
+                },
+                {
+                    type: shortCachesActionTypes.UPSERT,
+                    items: changedItems.caches
+                },
+                {
+                    type: clustersActionTypes.UPSERT,
+                    items: [changedItems.cluster]
+                },
+                {
+                    type: shortClustersActionTypes.UPSERT,
+                    items: [this.Clusters.toShortCluster(changedItems.cluster)]
+                }
+                ].filter((a) => a.items.length);
+
+                return merge(
+                    of(...actions),
+                    from(this.Clusters.saveBasic(changedItems)).pipe(
+                        switchMap((res) => of(
+                            {type: 'EDIT_CLUSTER', cluster: changedItems.cluster},
+                            basicSaveOK(changedItems)
+                        )),
+                        catchError((res) => of(
+                            basicSaveErr(changedItems, res),
+                            {type: 'UNDO_ACTIONS', actions}
+                        ))
+                    )
+                );
+            })
+        );
+
+        this.basicSaveOKMessagesEffect$ = this.ConfigureState.actions$.pipe(
+            ofType(BASIC_SAVE_OK),
+            tap((action) => this.IgniteMessages.showInfo(`Cluster "${action.changedItems.cluster.name}" saved.`)),
+            ignoreElements()
+        );
+    }
+
+    /**
+     * @name etp
+     * @function
+     * @param {object} action
+     * @returns {Promise}
+     */
+    /**
+     * @name etp^2
+     * @function
+     * @param {string} type
+     * @param {object} [params]
+     * @returns {Promise}
+     */
+    etp = (...args) => {
+        const action = typeof args[0] === 'object' ? args[0] : {type: args[0], ...args[1]};
+        const ok = `${action.type}_OK`;
+        const err = `${action.type}_ERR`;
+
+        setTimeout(() => this.ConfigureState.dispatchAction(action));
+
+        return this.ConfigureState.actions$.pipe(
+            filter((a) => a.type === ok || a.type === err),
+            take(1),
+            map((a) => {
+                if (a.type === err)
+                    throw a;
+                else
+                    return a;
+            })
+        ).toPromise();
+    };
+
+    connect() {
+        return merge(
+            ...Object.keys(this).filter((k) => k.endsWith('Effect$')).map((k) => this[k])
+        ).pipe(tap((a) => this.ConfigureState.dispatchAction(a))).subscribe();
+    }
+}
diff --git a/modules/frontend/app/configuration/store/effects.spec.js b/modules/frontend/app/configuration/store/effects.spec.js
new file mode 100644
index 0000000..f8b3909
--- /dev/null
+++ b/modules/frontend/app/configuration/store/effects.spec.js
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {assert} from 'chai';
+import {of, throwError} from 'rxjs';
+import {TestScheduler} from 'rxjs/testing';
+import {default as Effects} from './effects';
+import {default as Selectors} from './selectors';
+
+const makeMocks = (target, mocks) => new Map(target.$inject.map((provider) => {
+    return (provider in mocks) ? [provider, mocks[provider]] : [provider, {}];
+}));
+
+suite('Configuration store effects', () => {
+    suite('Load and edit cluster', () => {
+        const actionValues = {
+            a: {type: 'LOAD_AND_EDIT_CLUSTER', clusterID: 'new'},
+            b: {type: 'LOAD_AND_EDIT_CLUSTER', clusterID: '1'},
+            c: {type: 'LOAD_AND_EDIT_CLUSTER', clusterID: '2'}
+        };
+
+        const stateValues = {
+            A: {
+                shortClusters: {value: new Map()},
+                clusters: new Map()
+            },
+            B: {
+                shortClusters: {value: new Map([['1', {id: '1', name: 'Cluster'}]])},
+                clusters: new Map([['1', {id: '1', name: 'Cluster'}]])
+            }
+        };
+
+        const setup = ({actionMarbles, stateMarbles, mocks}) => {
+            const testScheduler = new TestScheduler((actual, expected) => assert.deepEqual(actual, expected));
+            const mocksMap = makeMocks(Effects, {
+                ...mocks,
+                ConfigureState: (() => {
+                    const actions$ = testScheduler.createHotObservable(actionMarbles, actionValues);
+                    const state$ = testScheduler.createHotObservable(stateMarbles, stateValues);
+                    return {actions$, state$};
+                })()
+            });
+
+            const effects = new Effects(...mocksMap.values());
+
+            return {testScheduler, effects};
+        };
+
+        const mocks = {
+            Clusters: {
+                getBlankCluster: () => ({id: 'foo'}),
+                getCluster: (id) => of({data: {id}})
+            },
+            ConfigSelectors: new Selectors()
+        };
+
+        test('New cluster', () => {
+            const {testScheduler, effects} = setup({
+                actionMarbles: '-a',
+                stateMarbles: 'B-',
+                mocks
+            });
+
+            testScheduler.expectObservable(effects.loadAndEditClusterEffect$).toBe('-(ab)', {
+                a: {type: 'EDIT_CLUSTER', cluster: {id: 'foo', name: 'Cluster1'}},
+                b: {type: 'LOAD_AND_EDIT_CLUSTER_OK'}
+            });
+
+            testScheduler.flush();
+        });
+
+        test('Cached cluster', () => {
+            const {testScheduler, effects} = setup({
+                actionMarbles: '-b',
+                stateMarbles: 'AB',
+                mocks
+            });
+
+            testScheduler.expectObservable(effects.loadAndEditClusterEffect$).toBe('-(ab)', {
+                a: {type: 'EDIT_CLUSTER', cluster: {id: '1', name: 'Cluster'}},
+                b: {type: 'LOAD_AND_EDIT_CLUSTER_OK'}
+            });
+
+            testScheduler.flush();
+        });
+
+        test('Cluster from server, success', () => {
+            const {testScheduler, effects} = setup({
+                actionMarbles: '-c',
+                stateMarbles: 'AB',
+                mocks
+            });
+
+            testScheduler.expectObservable(effects.loadAndEditClusterEffect$).toBe('-(abc)', {
+                a: {type: 'UPSERT_CLUSTERS', items: [{id: '2'}]},
+                b: {type: 'EDIT_CLUSTER', cluster: {id: '2'}},
+                c: {type: 'LOAD_AND_EDIT_CLUSTER_OK'}
+            });
+
+            testScheduler.flush();
+        });
+
+        test('Cluster from server, error', () => {
+            const {testScheduler, effects} = setup({
+                actionMarbles: '-c',
+                stateMarbles: 'AB',
+                mocks: {
+                    ...mocks,
+                    Clusters: {getCluster: () => throwError({data: 'Error'})}
+                }
+            });
+
+            testScheduler.expectObservable(effects.loadAndEditClusterEffect$).toBe('-a', {
+                a: {type: 'LOAD_AND_EDIT_CLUSTER_ERR', error: {message: `Failed to load cluster: Error.`}}
+            });
+
+            testScheduler.flush();
+        });
+    });
+});
diff --git a/modules/frontend/app/configuration/store/reducer.js b/modules/frontend/app/configuration/store/reducer.js
new file mode 100644
index 0000000..10374a9
--- /dev/null
+++ b/modules/frontend/app/configuration/store/reducer.js
@@ -0,0 +1,499 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import difference from 'lodash/difference';
+import capitalize from 'lodash/capitalize';
+import {REMOVE_CLUSTER_ITEMS_CONFIRMED} from './actionTypes';
+
+export const LOAD_LIST = Symbol('LOAD_LIST');
+export const ADD_CLUSTER = Symbol('ADD_CLUSTER');
+export const ADD_CLUSTERS = Symbol('ADD_CLUSTERS');
+export const REMOVE_CLUSTERS = Symbol('REMOVE_CLUSTERS');
+export const UPDATE_CLUSTER = Symbol('UPDATE_CLUSTER');
+export const UPSERT_CLUSTERS = Symbol('UPSERT_CLUSTERS');
+export const ADD_CACHE = Symbol('ADD_CACHE');
+export const UPDATE_CACHE = Symbol('UPDATE_CACHE');
+export const UPSERT_CACHES = Symbol('UPSERT_CACHES');
+export const REMOVE_CACHE = Symbol('REMOVE_CACHE');
+
+const defaults = {clusters: new Map(), caches: new Map(), spaces: new Map()};
+
+const mapByID = (items) => {
+    return Array.isArray(items) ? new Map(items.map((item) => [item._id, item])) : new Map(items);
+};
+
+export const reducer = (state = defaults, action) => {
+    switch (action.type) {
+        case LOAD_LIST: {
+            return {
+                clusters: mapByID(action.list.clusters),
+                domains: mapByID(action.list.domains),
+                caches: mapByID(action.list.caches),
+                spaces: mapByID(action.list.spaces),
+                plugins: mapByID(action.list.plugins)
+            };
+        }
+
+        case ADD_CLUSTER: {
+            return Object.assign({}, state, {
+                clusters: new Map([...state.clusters.entries(), [action.cluster._id, action.cluster]])
+            });
+        }
+
+        case ADD_CLUSTERS: {
+            return Object.assign({}, state, {
+                clusters: new Map([...state.clusters.entries(), ...action.clusters.map((c) => [c._id, c])])
+            });
+        }
+
+        case REMOVE_CLUSTERS: {
+            return Object.assign({}, state, {
+                clusters: new Map([...state.clusters.entries()].filter(([id, value]) => !action.clusterIDs.includes(id)))
+            });
+        }
+
+        case UPDATE_CLUSTER: {
+            const id = action._id || action.cluster._id;
+            return Object.assign({}, state, {
+                // clusters: new Map(state.clusters).set(id, Object.assign({}, state.clusters.get(id), action.cluster))
+                clusters: new Map(Array.from(state.clusters.entries()).map(([_id, cluster]) => {
+                    return _id === id
+                        ? [action.cluster._id || _id, Object.assign({}, cluster, action.cluster)]
+                        : [_id, cluster];
+                }))
+            });
+        }
+
+        case UPSERT_CLUSTERS: {
+            return action.clusters.reduce((state, cluster) => reducer(state, {
+                type: state.clusters.has(cluster._id) ? UPDATE_CLUSTER : ADD_CLUSTER,
+                cluster
+            }), state);
+        }
+
+        case ADD_CACHE: {
+            return Object.assign({}, state, {
+                caches: new Map([...state.caches.entries(), [action.cache._id, action.cache]])
+            });
+        }
+
+        case UPDATE_CACHE: {
+            const id = action.cache._id;
+
+            return Object.assign({}, state, {
+                caches: new Map(state.caches).set(id, Object.assign({}, state.caches.get(id), action.cache))
+            });
+        }
+
+        case UPSERT_CACHES: {
+            return action.caches.reduce((state, cache) => reducer(state, {
+                type: state.caches.has(cache._id) ? UPDATE_CACHE : ADD_CACHE,
+                cache
+            }), state);
+        }
+
+        case REMOVE_CACHE:
+            return state;
+
+        default:
+            return state;
+    }
+};
+
+
+export const RECEIVE_CLUSTER_EDIT = Symbol('RECEIVE_CLUSTER_EDIT');
+export const RECEIVE_CACHE_EDIT = Symbol('RECEIVE_CACHE_EDIT');
+export const RECEIVE_IGFSS_EDIT = Symbol('RECEIVE_IGFSS_EDIT');
+export const RECEIVE_IGFS_EDIT = Symbol('RECEIVE_IGFS_EDIT');
+export const RECEIVE_MODELS_EDIT = Symbol('RECEIVE_MODELS_EDIT');
+export const RECEIVE_MODEL_EDIT = Symbol('RECEIVE_MODEL_EDIT');
+
+export const editReducer = (state = {originalCluster: null}, action) => {
+    switch (action.type) {
+        case RECEIVE_CLUSTER_EDIT:
+            return {
+                ...state,
+                originalCluster: action.cluster
+            };
+
+        case RECEIVE_CACHE_EDIT: {
+            return {
+                ...state,
+                originalCache: action.cache
+            };
+        }
+
+        case RECEIVE_IGFSS_EDIT:
+            return {
+                ...state,
+                originalIGFSs: action.igfss
+            };
+
+        case RECEIVE_IGFS_EDIT: {
+            return {
+                ...state,
+                originalIGFS: action.igfs
+            };
+        }
+
+        case RECEIVE_MODELS_EDIT:
+            return {
+                ...state,
+                originalModels: action.models
+            };
+
+        case RECEIVE_MODEL_EDIT: {
+            return {
+                ...state,
+                originalModel: action.model
+            };
+        }
+
+        default:
+            return state;
+    }
+};
+
+export const SHOW_CONFIG_LOADING = Symbol('SHOW_CONFIG_LOADING');
+export const HIDE_CONFIG_LOADING = Symbol('HIDE_CONFIG_LOADING');
+const loadingDefaults = {isLoading: false, loadingText: 'Loading...'};
+
+export const loadingReducer = (state = loadingDefaults, action) => {
+    switch (action.type) {
+        case SHOW_CONFIG_LOADING:
+            return {...state, isLoading: true, loadingText: action.loadingText};
+
+        case HIDE_CONFIG_LOADING:
+            return {...state, isLoading: false};
+
+        default:
+            return state;
+    }
+};
+
+export const setStoreReducerFactory = (actionTypes) => (state = new Set(), action = {}) => {
+    switch (action.type) {
+        case actionTypes.SET:
+            return new Set(action.items.map((i) => i._id));
+
+        case actionTypes.RESET:
+            return new Set();
+
+        case actionTypes.UPSERT:
+            return action.items.reduce((acc, item) => {acc.add(item._id); return acc;}, new Set(state));
+
+        case actionTypes.REMOVE:
+            return action.items.reduce((acc, item) => {acc.delete(item); return acc;}, new Set(state));
+
+        default:
+            return state;
+    }
+};
+
+export const mapStoreReducerFactory = (actionTypes) => (state = new Map(), action = {}) => {
+    switch (action.type) {
+        case actionTypes.SET:
+            return new Map(action.items.map((i) => [i._id, i]));
+
+        case actionTypes.RESET:
+            return new Map();
+
+        case actionTypes.UPSERT:
+            if (!action.items.length)
+                return state;
+
+            return action.items.reduce((acc, item) => {acc.set(item._id, item); return acc;}, new Map(state));
+
+        case actionTypes.REMOVE:
+            if (!action.ids.length)
+                return state;
+
+            return action.ids.reduce((acc, id) => {acc.delete(id); return acc;}, new Map(state));
+
+        default:
+            return state;
+    }
+};
+
+export const mapCacheReducerFactory = (actionTypes) => {
+    const mapStoreReducer = mapStoreReducerFactory(actionTypes);
+
+    return (state = {value: mapStoreReducer(), pristine: true}, action) => {
+        switch (action.type) {
+            case actionTypes.SET:
+            case actionTypes.REMOVE:
+            case actionTypes.UPSERT:
+                return {
+                    value: mapStoreReducer(state.value, action),
+                    pristine: false
+                };
+
+            case actionTypes.RESET:
+                return {
+                    value: mapStoreReducer(state.value, action),
+                    pristine: true
+                };
+
+            default:
+                return state;
+        }
+    };
+};
+
+export const basicCachesActionTypes = {
+    SET: 'SET_BASIC_CACHES',
+    RESET: 'RESET_BASIC_CACHES',
+    LOAD: 'LOAD_BASIC_CACHES',
+    UPSERT: 'UPSERT_BASIC_CACHES',
+    REMOVE: 'REMOVE_BASIC_CACHES'
+};
+
+export const mapStoreActionTypesFactory = (NAME) => ({
+    SET: `SET_${NAME}`,
+    RESET: `RESET_${NAME}`,
+    UPSERT: `UPSERT_${NAME}`,
+    REMOVE: `REMOVE_${NAME}`
+});
+
+export const clustersActionTypes = mapStoreActionTypesFactory('CLUSTERS');
+export const shortClustersActionTypes = mapStoreActionTypesFactory('SHORT_CLUSTERS');
+export const cachesActionTypes = mapStoreActionTypesFactory('CACHES');
+export const shortCachesActionTypes = mapStoreActionTypesFactory('SHORT_CACHES');
+export const modelsActionTypes = mapStoreActionTypesFactory('MODELS');
+export const shortModelsActionTypes = mapStoreActionTypesFactory('SHORT_MODELS');
+export const igfssActionTypes = mapStoreActionTypesFactory('IGFSS');
+export const shortIGFSsActionTypes = mapStoreActionTypesFactory('SHORT_IGFSS');
+
+export const itemsEditReducerFactory = (actionTypes) => {
+    const setStoreReducer = setStoreReducerFactory(actionTypes);
+    const mapStoreReducer = mapStoreReducerFactory(actionTypes);
+
+    return (state = {ids: setStoreReducer(), changedItems: mapStoreReducer()}, action) => {
+        switch (action.type) {
+            case actionTypes.SET:
+                return action.state;
+
+            case actionTypes.LOAD:
+                return {
+                    ...state,
+                    ids: setStoreReducer(state.ids, {...action, type: actionTypes.UPSERT})
+                };
+
+            case actionTypes.RESET:
+            case actionTypes.UPSERT:
+                return {
+                    ids: setStoreReducer(state.ids, action),
+                    changedItems: mapStoreReducer(state.changedItems, action)
+                };
+
+            case actionTypes.REMOVE:
+                return {
+                    ids: setStoreReducer(state.ids, {type: action.type, items: action.ids}),
+                    changedItems: mapStoreReducer(state.changedItems, action)
+                };
+
+            default:
+                return state;
+        }
+    };
+};
+
+export const editReducer2 = (state = editReducer2.getDefaults(), action) => {
+    switch (action.type) {
+        case 'SET_EDIT':
+            return action.state;
+
+        case 'EDIT_CLUSTER': {
+            return {
+                ...state,
+                changes: {
+                    ...['caches', 'models', 'igfss'].reduce((a, t) => ({
+                        ...a,
+                        [t]: {
+                            ids: action.cluster ? action.cluster[t] || [] : [],
+                            changedItems: []
+                        }
+                    }), state.changes),
+                    cluster: action.cluster
+                }
+            };
+        }
+
+        case 'RESET_EDIT_CHANGES': {
+            return {
+                ...state,
+                changes: {
+                    ...['caches', 'models', 'igfss'].reduce((a, t) => ({
+                        ...a,
+                        [t]: {
+                            ids: state.changes.cluster ? state.changes.cluster[t] || [] : [],
+                            changedItems: []
+                        }
+                    }), state.changes),
+                    cluster: {...state.changes.cluster}
+                }
+            };
+        }
+
+        case 'UPSERT_CLUSTER': {
+            return {
+                ...state,
+                changes: {
+                    ...state.changes,
+                    cluster: action.cluster
+                }
+            };
+        }
+
+        case 'UPSERT_CLUSTER_ITEM': {
+            const {itemType, item} = action;
+            return {
+                ...state,
+                changes: {
+                    ...state.changes,
+                    [itemType]: {
+                        ids: state.changes[itemType].ids.filter((_id) => _id !== item._id).concat(item._id),
+                        changedItems: state.changes[itemType].changedItems.filter(({_id}) => _id !== item._id).concat(item)
+                    }
+                }
+            };
+        }
+
+        case REMOVE_CLUSTER_ITEMS_CONFIRMED: {
+            const {itemType, itemIDs} = action;
+
+            return {
+                ...state,
+                changes: {
+                    ...state.changes,
+                    [itemType]: {
+                        ids: state.changes[itemType].ids.filter((_id) => !itemIDs.includes(_id)),
+                        changedItems: state.changes[itemType].changedItems.filter(({_id}) => !itemIDs.includes(_id))
+                    }
+                }
+            };
+        }
+
+        default: return state;
+    }
+};
+
+editReducer2.getDefaults = () => ({
+    changes: ['caches', 'models', 'igfss'].reduce((a, t) => ({...a, [t]: {ids: [], changedItems: []}}), {cluster: null})
+});
+
+export const refsReducer = (refs) => (state, action) => {
+    switch (action.type) {
+        case 'ADVANCED_SAVE_COMPLETE_CONFIGURATION': {
+            const newCluster = action.changedItems.cluster;
+            const oldCluster = state.clusters.get(newCluster._id) || {};
+            const val = Object.keys(refs).reduce((state, ref) => {
+                if (!state || !state[refs[ref].store].size)
+                    return state;
+
+                const addedSources = new Set(difference(newCluster[ref], oldCluster[ref] || []));
+                const removedSources = new Set(difference(oldCluster[ref] || [], newCluster[ref]));
+                const changedSources = new Map(action.changedItems[ref].map((m) => [m._id, m]));
+
+                const targets = new Map();
+
+                const maybeTarget = (id) => {
+                    if (!targets.has(id))
+                        targets.set(id, {[refs[ref].at]: {add: new Set(), remove: new Set()}});
+
+                    return targets.get(id);
+                };
+
+                [...state[refs[ref].store].values()].forEach((target) => {
+                    target[refs[ref].at]
+                    .filter((sourceID) => removedSources.has(sourceID))
+                    .forEach((sourceID) => maybeTarget(target._id)[refs[ref].at].remove.add(sourceID));
+                });
+
+                [...addedSources.values()].forEach((sourceID) => {
+                    (changedSources.get(sourceID)[refs[ref].store] || []).forEach((targetID) => {
+                        maybeTarget(targetID)[refs[ref].at].add.add(sourceID);
+                    });
+                });
+
+                action.changedItems[ref].filter((s) => !addedSources.has(s._id)).forEach((source) => {
+                    const newSource = source;
+                    const oldSource = state[ref].get(source._id);
+                    const addedTargets = difference(newSource[refs[ref].store], oldSource[refs[ref].store]);
+                    const removedCaches = difference(oldSource[refs[ref].store], newSource[refs[ref].store]);
+                    addedTargets.forEach((targetID) => {
+                        maybeTarget(targetID)[refs[ref].at].add.add(source._id);
+                    });
+                    removedCaches.forEach((targetID) => {
+                        maybeTarget(targetID)[refs[ref].at].remove.add(source._id);
+                    });
+                });
+                const result = [...targets.entries()]
+                    .filter(([targetID]) => state[refs[ref].store].has(targetID))
+                    .map(([targetID, changes]) => {
+                        const target = state[refs[ref].store].get(targetID);
+                        return [
+                            targetID,
+                            {
+                                ...target,
+                                [refs[ref].at]: target[refs[ref].at]
+                                    .filter((sourceID) => !changes[refs[ref].at].remove.has(sourceID))
+                                    .concat([...changes[refs[ref].at].add.values()])
+                            }
+                        ];
+                    });
+
+                return result.length
+                    ? {
+                        ...state,
+                        [refs[ref].store]: new Map([...state[refs[ref].store].entries()].concat(result))
+                    }
+                    : state;
+            }, state);
+
+            return val;
+        }
+
+        default:
+            return state;
+    }
+};
+
+export const shortObjectsReducer = (state, action) => {
+    switch (action.type) {
+        case REMOVE_CLUSTER_ITEMS_CONFIRMED: {
+            const {itemType, itemIDs} = action;
+
+            const target = 'short' + capitalize(itemType);
+
+            const oldItems = state[target];
+
+            const newItems = {
+                value: itemIDs.reduce((acc, id) => {acc.delete(id); return acc;}, oldItems.value),
+                pristine: oldItems.pristine
+            };
+
+            return {
+                ...state,
+                [target]: newItems
+            };
+        }
+
+        default:
+            return state;
+    }
+};
diff --git a/modules/frontend/app/configuration/store/reducer.spec.js b/modules/frontend/app/configuration/store/reducer.spec.js
new file mode 100644
index 0000000..e969da9
--- /dev/null
+++ b/modules/frontend/app/configuration/store/reducer.spec.js
@@ -0,0 +1,275 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {suite, test} from 'mocha';
+import {assert} from 'chai';
+
+import {
+    ADD_CACHE,
+    ADD_CLUSTER,
+    reducer,
+    REMOVE_CACHE,
+    REMOVE_CLUSTERS,
+    UPDATE_CACHE,
+    UPDATE_CLUSTER,
+    UPSERT_CACHES,
+    UPSERT_CLUSTERS
+} from './reducer';
+
+suite('page-configure component reducer', () => {
+    test('Default state', () => {
+        assert.deepEqual(
+            reducer(void 0, {}),
+            {
+                clusters: new Map(),
+                caches: new Map(),
+                spaces: new Map()
+            }
+        );
+    });
+
+    test('ADD_CLUSTER action', () => {
+        assert.deepEqual(
+            reducer(
+                {clusters: new Map([[1, {_id: 1}], [2, {_id: 2}]])},
+                {type: ADD_CLUSTER, cluster: {_id: 3}}
+            ),
+            {
+                clusters: new Map([[1, {_id: 1}], [2, {_id: 2}], [3, {_id: 3}]])
+            },
+            'adds a cluster'
+        );
+    });
+
+    test('REMOVE_CLUSTERS action', () => {
+        assert.deepEqual(
+            reducer(
+                {clusters: new Map([[1, {_id: 1, name: 'Cluster 1'}], [2, {_id: 2, name: 'Cluster 2'}]])},
+                {type: REMOVE_CLUSTERS, clusterIDs: [1]}
+            ),
+            {clusters: new Map([[2, {_id: 2, name: 'Cluster 2'}]])},
+            'deletes clusters by id'
+        );
+    });
+
+    test('UPDATE_CLUSTER action', () => {
+        assert.deepEqual(
+            reducer(
+                {clusters: new Map([[1, {_id: 1, name: 'Hello'}]])},
+                {type: UPDATE_CLUSTER, cluster: {_id: 1, name: 'Hello world'}}
+            ),
+            {clusters: new Map([[1, {_id: 1, name: 'Hello world'}]])},
+            'updates a cluster'
+        );
+    });
+
+    test('UPSERT_CLUSTERS', () => {
+        assert.deepEqual(
+            reducer(
+                {clusters: new Map([
+                    [1, {_id: 1, name: 'One'}],
+                    [2, {_id: 2, name: 'Two'}]
+                ])},
+                {type: UPSERT_CLUSTERS, clusters: [{_id: 1, name: '1', space: 1}]}
+            ),
+            {clusters: new Map([
+                [1, {_id: 1, name: '1', space: 1}],
+                [2, {_id: 2, name: 'Two'}]
+            ])},
+            'updates one cluster'
+        );
+
+        assert.deepEqual(
+            reducer(
+                {clusters: new Map([
+                    [1, {_id: 1, name: 'One'}],
+                    [2, {_id: 2, name: 'Two'}]
+                ])},
+                {
+                    type: UPSERT_CLUSTERS,
+                    clusters: [
+                        {_id: 1, name: '1', space: 1},
+                        {_id: 2, name: '2'}
+                    ]
+                }
+            ),
+            {clusters: new Map([
+                [1, {_id: 1, name: '1', space: 1}],
+                [2, {_id: 2, name: '2'}]
+            ])},
+            'updates two clusters'
+        );
+
+        assert.deepEqual(
+            reducer(
+                {clusters: new Map()},
+                {type: UPSERT_CLUSTERS, clusters: [{_id: 1}]}
+            ),
+            {clusters: new Map([
+                [1, {_id: 1}]
+            ])},
+            'adds one cluster'
+        );
+
+        assert.deepEqual(
+            reducer(
+                {clusters: new Map([[1, {_id: 1}]])},
+                {type: UPSERT_CLUSTERS, clusters: [{_id: 2}, {_id: 3}]}
+            ),
+            {clusters: new Map([
+                [1, {_id: 1}],
+                [2, {_id: 2}],
+                [3, {_id: 3}]
+            ])},
+            'adds two clusters'
+        );
+
+        assert.deepEqual(
+            reducer(
+                {clusters: new Map([[1, {_id: 1}]])},
+                {
+                    type: UPSERT_CLUSTERS,
+                    clusters: [
+                        {_id: 1, name: 'Test'},
+                        {_id: 2},
+                        {_id: 3}
+                    ]
+                }
+            ),
+            {clusters: new Map([
+                [1, {_id: 1, name: 'Test'}],
+                [2, {_id: 2}],
+                [3, {_id: 3}]
+            ])},
+            'adds and updates several clusters'
+        );
+    });
+
+    test('ADD_CACHE action', () => {
+        assert.deepEqual(
+            reducer(
+                {caches: new Map([[1, {_id: 1}], [2, {_id: 2}]])},
+                {type: ADD_CACHE, cache: {_id: 3}}
+            ),
+            {
+                caches: new Map([[1, {_id: 1}], [2, {_id: 2}], [3, {_id: 3}]])
+            },
+            'adds a cache'
+        );
+    });
+
+    test('REMOVE_CACHE action', () => {
+        assert.deepEqual(
+            reducer({}, {type: REMOVE_CACHE}),
+            {},
+            'does nothing yet'
+        );
+    });
+
+    test('UPDATE_CACHE action', () => {
+        assert.deepEqual(
+            reducer(
+                {caches: new Map([[1, {_id: 1, name: 'Hello'}]])},
+                {type: UPDATE_CACHE, cache: {_id: 1, name: 'Hello world'}}
+            ),
+            {caches: new Map([[1, {_id: 1, name: 'Hello world'}]])},
+            'updates a cache'
+        );
+    });
+
+    test('UPSERT_CACHES', () => {
+        assert.deepEqual(
+            reducer(
+                {caches: new Map([
+                    [1, {_id: 1, name: 'One'}],
+                    [2, {_id: 2, name: 'Two'}]
+                ])},
+                {type: UPSERT_CACHES, caches: [{_id: 1, name: '1', space: 1}]}
+            ),
+            {caches: new Map([
+                [1, {_id: 1, name: '1', space: 1}],
+                [2, {_id: 2, name: 'Two'}]
+            ])},
+            'updates one cache'
+        );
+
+        assert.deepEqual(
+            reducer(
+                {caches: new Map([
+                    [1, {_id: 1, name: 'One'}],
+                    [2, {_id: 2, name: 'Two'}]
+                ])},
+                {
+                    type: UPSERT_CACHES,
+                    caches: [
+                        {_id: 1, name: '1', space: 1},
+                        {_id: 2, name: '2'}
+                    ]
+                }
+            ),
+            {caches: new Map([
+                [1, {_id: 1, name: '1', space: 1}],
+                [2, {_id: 2, name: '2'}]
+            ])},
+            'updates two caches'
+        );
+
+        assert.deepEqual(
+            reducer(
+                {caches: new Map()},
+                {type: UPSERT_CACHES, caches: [{_id: 1}]}
+            ),
+            {caches: new Map([
+                [1, {_id: 1}]
+            ])},
+            'adds one cache'
+        );
+
+        assert.deepEqual(
+            reducer(
+                {caches: new Map([[1, {_id: 1}]])},
+                {type: UPSERT_CACHES, caches: [{_id: 2}, {_id: 3}]}
+            ),
+            {caches: new Map([
+                [1, {_id: 1}],
+                [2, {_id: 2}],
+                [3, {_id: 3}]
+            ])},
+            'adds two caches'
+        );
+
+        assert.deepEqual(
+            reducer(
+                {caches: new Map([[1, {_id: 1}]])},
+                {
+                    type: UPSERT_CACHES,
+                    caches: [
+                        {_id: 1, name: 'Test'},
+                        {_id: 2},
+                        {_id: 3}
+                    ]
+                }
+            ),
+            {caches: new Map([
+                [1, {_id: 1, name: 'Test'}],
+                [2, {_id: 2}],
+                [3, {_id: 3}]
+            ])},
+            'adds and updates several caches'
+        );
+    });
+});
diff --git a/modules/frontend/app/configuration/store/selectors.ts b/modules/frontend/app/configuration/store/selectors.ts
new file mode 100644
index 0000000..53bd0f8
--- /dev/null
+++ b/modules/frontend/app/configuration/store/selectors.ts
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {uniqueName} from 'app/utils/uniqueName';
+import {combineLatest, empty, forkJoin, of, pipe} from 'rxjs';
+import {distinctUntilChanged, exhaustMap, filter, map, pluck, switchMap, take} from 'rxjs/operators';
+import {defaultNames} from '../defaultNames';
+
+import {default as Caches} from '../services/Caches';
+import {default as Clusters} from '../services/Clusters';
+import {default as IGFSs} from '../services/IGFSs';
+import {default as Models} from '../services/Models';
+
+const isDefined = filter((v) => v);
+
+const selectItems = (path) => pipe(filter((s) => s), pluck(path), filter((v) => v));
+
+const selectValues = map((v) => v && [...v.value.values()]);
+
+export const selectMapItem = (mapPath, key) => pipe(pluck(mapPath), map((v) => v && v.get(key)));
+
+const selectMapItems = (mapPath, keys) => pipe(pluck(mapPath), map((v) => v && keys.map((key) => v.get(key))));
+
+const selectItemToEdit = ({items, itemFactory, defaultName = '', itemID}) => switchMap((item) => {
+    if (item)
+        return of(Object.assign(itemFactory(), item));
+
+    if (itemID === 'new')
+        return items.pipe(take(1), map((items) => Object.assign(itemFactory(), {name: uniqueName(defaultName, items)})));
+
+    if (!itemID)
+        return of(null);
+
+    return empty();
+});
+
+const currentShortItems = ({changesKey, shortKey}) => (state$) => {
+    return combineLatest(
+        state$.pipe(pluck('edit', 'changes', changesKey), isDefined, distinctUntilChanged()),
+        state$.pipe(pluck(shortKey, 'value'), isDefined, distinctUntilChanged())
+    ).pipe(
+        map(([{ids = [], changedItems}, shortItems]) => {
+            if (!ids.length || !shortItems)
+                return [];
+
+            return ids.map((id) => changedItems.find(({_id}) => _id === id) || shortItems.get(id));
+        }),
+        map((v) => v.filter((v) => v))
+    );
+};
+
+const selectNames = (itemIDs, nameAt = 'name') => pipe(
+    pluck('value'),
+    map((items) => itemIDs.map((id) => items.get(id)[nameAt]))
+);
+
+export default class ConfigSelectors {
+    static $inject = ['Caches', 'Clusters', 'IGFSs', 'Models'];
+
+    /**
+     * @param {Caches} Caches
+     * @param {Clusters} Clusters
+     * @param {IGFSs} IGFSs
+     * @param {Models} Models
+     */
+    constructor(private Caches: Caches, private Clusters: Clusters, private IGFSs: IGFSs, private Models: Models) {}
+
+    /**
+     * @returns {(state$: Observable) => Observable<DomainModel>}
+     */
+    selectModel = (id: string) => selectMapItem('models', id);
+
+    /**
+     * @returns {(state$: Observable) => Observable<{pristine: boolean, value: Map<string, ShortDomainModel>}>}
+     */
+    selectShortModels = () => selectItems('shortModels');
+
+    selectShortModelsValue = () => (state$) => state$.pipe(this.selectShortModels(), selectValues);
+
+    /**
+     * @returns {(state$: Observable) => Observable<Array<ShortCluster>>}
+     */
+    selectShortClustersValue = () => (state$) => state$.pipe(this.selectShortClusters(), selectValues);
+
+    /**
+     * @returns {(state$: Observable) => Observable<Array<string>>}
+     */
+    selectClusterNames = (clusterIDs) => (state$) => state$.pipe(
+        this.selectShortClusters(),
+        selectNames(clusterIDs)
+    );
+
+    selectCluster = (id) => selectMapItem('clusters', id);
+
+    selectShortClusters = () => selectItems('shortClusters');
+
+    selectCache = (id) => selectMapItem('caches', id);
+
+    selectIGFS = (id) => selectMapItem('igfss', id);
+
+    selectShortCaches = () => selectItems('shortCaches');
+
+    selectShortCachesValue = () => (state$) => state$.pipe(this.selectShortCaches(), selectValues);
+
+    selectShortIGFSs = () => selectItems('shortIgfss');
+
+    selectShortIGFSsValue = () => (state$) => state$.pipe(this.selectShortIGFSs(), selectValues);
+
+    selectShortModelsValue = () => (state$) => state$.pipe(this.selectShortModels(), selectValues);
+
+    selectCacheToEdit = (cacheID) => (state$) => state$.pipe(
+        this.selectCache(cacheID),
+        distinctUntilChanged(),
+        selectItemToEdit({
+            items: state$.pipe(this.selectCurrentShortCaches),
+            itemFactory: () => this.Caches.getBlankCache(),
+            defaultName: defaultNames.cache,
+            itemID: cacheID
+        })
+    );
+
+    selectIGFSToEdit = (itemID) => (state$) => state$.pipe(
+        this.selectIGFS(itemID),
+        distinctUntilChanged(),
+        selectItemToEdit({
+            items: state$.pipe(this.selectCurrentShortIGFSs),
+            itemFactory: () => this.IGFSs.getBlankIGFS(),
+            defaultName: defaultNames.igfs,
+            itemID
+        })
+    );
+
+    selectModelToEdit = (itemID) => (state$) => state$.pipe(
+        this.selectModel(itemID),
+        distinctUntilChanged(),
+        selectItemToEdit({
+            items: state$.pipe(this.selectCurrentShortModels),
+            itemFactory: () => this.Models.getBlankModel(),
+            itemID
+        })
+    );
+
+    selectClusterToEdit = (clusterID, defaultName = defaultNames.cluster) => (state$) => state$.pipe(
+        this.selectCluster(clusterID),
+        distinctUntilChanged(),
+        selectItemToEdit({
+            items: state$.pipe(this.selectShortClustersValue()),
+            itemFactory: () => this.Clusters.getBlankCluster(),
+            defaultName,
+            itemID: clusterID
+        })
+    );
+
+    selectCurrentShortCaches = currentShortItems({changesKey: 'caches', shortKey: 'shortCaches'});
+
+    selectCurrentShortIGFSs = currentShortItems({changesKey: 'igfss', shortKey: 'shortIgfss'});
+
+    selectCurrentShortModels = currentShortItems({changesKey: 'models', shortKey: 'shortModels'});
+
+    selectClusterShortCaches = (clusterID) => (state$) => {
+        if (clusterID === 'new')
+            return of([]);
+
+        return combineLatest(
+            state$.pipe(this.selectCluster(clusterID), pluck('caches')),
+            state$.pipe(this.selectShortCaches(), pluck('value')),
+            (ids, items) => ids.map((id) => items.get(id))
+        );
+    };
+
+    selectCompleteClusterConfiguration = ({clusterID, isDemo}) => (state$) => {
+        const hasValues = (array) => !array.some((v) => !v);
+        return state$.pipe(
+            this.selectCluster(clusterID),
+            exhaustMap((cluster) => {
+                if (!cluster)
+                    return of({__isComplete: false});
+
+                return forkJoin(
+                    state$.pipe(selectMapItems('caches', cluster.caches || []), take(1)),
+                    state$.pipe(selectMapItems('models', cluster.models || []), take(1)),
+                    state$.pipe(selectMapItems('igfss', cluster.igfss || []), take(1)),
+                ).pipe(map(([caches, models, igfss]) => ({
+                    cluster,
+                    caches,
+                    domains: models,
+                    igfss,
+                    spaces: [{_id: cluster.space, demo: isDemo}],
+                    __isComplete: !!cluster && !(!hasValues(caches) || !hasValues(models) || !hasValues(igfss))
+                })));
+            })
+        );
+    };
+}
diff --git a/modules/frontend/app/configuration/transitionHooks/errorState.ts b/modules/frontend/app/configuration/transitionHooks/errorState.ts
new file mode 100644
index 0000000..2de2d29
--- /dev/null
+++ b/modules/frontend/app/configuration/transitionHooks/errorState.ts
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {HookMatchCriteria, RejectType, Transition, UIRouter} from '@uirouter/angularjs';
+
+const isPromise = (object): object is Promise<any> => object && typeof object.then === 'function';
+const match: HookMatchCriteria = {
+    to(state) {
+        return state.data && state.data.errorState;
+    }
+};
+const go = ($transition: Transition) => $transition.router.stateService.go(
+    $transition.to().data.errorState,
+    $transition.params(),
+    {location: 'replace'}
+);
+
+const getResolvePromises = ($transition: Transition) => $transition.getResolveTokens()
+    .filter((token) => typeof token === 'string')
+    .map((token) => $transition.injector().getAsync(token))
+    .filter(isPromise);
+
+/**
+ * Global transition hook that redirects to data.errorState if:
+ * 1. Transition throws an error.
+ * 2. Any resolve promise throws an error. onError does not work for this case if resolvePolicy is set to 'NOWAIT'.
+ */
+export const errorState = ($uiRouter: UIRouter) => {
+    $uiRouter.transitionService.onError(match, ($transition) => {
+        if ($transition.error().type !== RejectType.ERROR)
+            return;
+
+        go($transition);
+    });
+
+    $uiRouter.transitionService.onStart(match, ($transition) => {
+        Promise.all(getResolvePromises($transition)).catch((e) => go($transition));
+    });
+};
+
+errorState.$inject = ['$uiRouter'];
diff --git a/modules/frontend/app/configuration/types/index.ts b/modules/frontend/app/configuration/types/index.ts
new file mode 100644
index 0000000..eb08993
--- /dev/null
+++ b/modules/frontend/app/configuration/types/index.ts
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Cache
+export type CacheModes = 'PARTITIONED' | 'REPLICATED' | 'LOCAL';
+export type AtomicityModes = 'ATOMIC' | 'TRANSACTIONAL' | 'TRANSACTIONAL_SNAPSHOT';
+
+export interface ShortCache {
+    _id: string,
+    cacheMode: CacheModes,
+    atomicityMode: AtomicityModes,
+    backups: number,
+    name: string
+}
+
+// IGFS
+type DefaultModes = 'PRIMARY' | 'PROXY' | 'DUAL_SYNC' | 'DUAL_ASYNC';
+
+export interface ShortIGFS {
+    _id: string,
+    name: string,
+    defaultMode: DefaultModes,
+    affinnityGroupSize: number
+}
+
+// Models
+type QueryMetadataTypes = 'Annotations' | 'Configuration';
+type DomainModelKinds = 'query' | 'store' | 'both';
+export interface KeyField {
+    databaseFieldName: string,
+    databaseFieldType: string,
+    javaFieldName: string,
+    javaFieldType: string
+}
+export interface ValueField {
+    databaseFieldName: string,
+    databaseFieldType: string,
+    javaFieldName: string,
+    javaFieldType: string
+}
+export interface Field {
+    name: string,
+    className: string
+}
+export interface Alias {
+    field: string,
+    alias: string
+}
+export type IndexTypes = 'SORTED' | 'FULLTEXT' | 'GEOSPATIAL';
+export interface IndexField {
+    _id: string,
+    name?: string,
+    direction?: boolean
+}
+export interface Index {
+    _id: string,
+    name: string,
+    indexType: IndexTypes,
+    fields: Array<IndexField>
+}
+
+export interface DomainModel {
+    _id: string,
+    space?: string,
+    clusters?: Array<string>,
+    caches?: Array<string>,
+    queryMetadata?: QueryMetadataTypes,
+    kind?: DomainModelKinds,
+    tableName?: string,
+    keyFieldName?: string,
+    valueFieldName?: string,
+    databaseSchema?: string,
+    databaseTable?: string,
+    keyType?: string,
+    valueType?: string,
+    keyFields?: Array<KeyField>,
+    valueFields?: Array<ValueField>,
+    queryKeyFields?: Array<string>,
+    fields?: Array<Field>,
+    aliases?: Array<Alias>,
+    indexes?: Array<Index>,
+    generatePojo?: boolean
+}
+
+export interface ShortDomainModel {
+    _id: string,
+    keyType: string,
+    valueType: string,
+    hasIndex: boolean
+}
+
+// Cluster
+export type DiscoveryKinds = 'Vm'
+    | 'Multicast'
+    | 'S3'
+    | 'Cloud'
+    | 'GoogleStorage'
+    | 'Jdbc'
+    | 'SharedFs'
+    | 'ZooKeeper'
+    | 'Kubernetes';
+
+export type LoadBalancingKinds = 'RoundRobin'
+    | 'Adaptive'
+    | 'WeightedRandom'
+    | 'Custom';
+
+export type FailoverSPIs = 'JobStealing' | 'Never' | 'Always' | 'Custom';
+
+export interface Cluster {
+    _id: string,
+    name: string,
+    // TODO: cover with types
+    [key: string]: any
+}
+
+export interface ShortCluster {
+    _id: string,
+    name: string,
+    discovery: DiscoveryKinds,
+    caches: number,
+    models: number,
+    igfs: number
+}
+
+export type ClusterLike = Cluster | ShortCluster;
diff --git a/modules/frontend/app/core/activities/Activities.data.ts b/modules/frontend/app/core/activities/Activities.data.ts
new file mode 100644
index 0000000..b8de6a7
--- /dev/null
+++ b/modules/frontend/app/core/activities/Activities.data.ts
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {StateService} from '@uirouter/angularjs';
+
+interface IActivityDataResponse {
+    action: string,
+    amount: number,
+    date: string,
+    group: string,
+    owner: string,
+    _id: string
+}
+
+export default class ActivitiesData {
+    static $inject = ['$http', '$state'];
+
+    constructor(private $http: ng.IHttpService, private $state: StateService) {}
+
+    /**
+     * Posts activity to backend, sends current state if no options specified
+     */
+    // For some reason, Babel loses this after destructuring, the arrow helps with that
+    post = (options: {group?: string, action?: string} = {}) => {
+        let { group, action } = options;
+
+        // TODO IGNITE-5466: since upgrade to UIRouter 1, "url.source" is undefined.
+        // Actions like that won't be saved to DB. Think of a better solution later.
+        action = action || this.$state.$current.url.source || '';
+        group = group || (action.match(/^\/([^/]+)/) || [])[1];
+
+        return this.$http.post<IActivityDataResponse>('/api/v1/activities/page', { group, action })
+            .catch(() => {
+                // No-op.
+            });
+    }
+}
diff --git a/modules/frontend/app/core/admin/Admin.data.js b/modules/frontend/app/core/admin/Admin.data.js
new file mode 100644
index 0000000..9ebe599
--- /dev/null
+++ b/modules/frontend/app/core/admin/Admin.data.js
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+export default class IgniteAdminData {
+    static $inject = ['$http', 'IgniteMessages', 'IgniteCountries'];
+
+    /**
+     * @param {ng.IHttpService} $http     
+     * @param {ReturnType<typeof import('app/services/Messages.service').default>} Messages
+     * @param {ReturnType<typeof import('app/services/Countries.service').default>} Countries
+     */
+    constructor($http, Messages, Countries) {
+        this.$http = $http;
+        this.Messages = Messages;
+        this.Countries = Countries;
+    }
+
+    /**
+     * @param {string} viewedUserId
+     */
+    becomeUser(viewedUserId) {
+        return this.$http.get('/api/v1/admin/become', {
+            params: {viewedUserId}
+        })
+        .catch(this.Messages.showError);
+    }
+
+    /**
+     * @param {import('app/modules/user/User.service').User} user
+     */
+    removeUser(user) {
+        return this.$http.post('/api/v1/admin/remove', {
+            userId: user._id
+        })
+        .then(() => {
+            this.Messages.showInfo(`User has been removed: "${user.userName}"`);
+        })
+        .catch(({data, status}) => {
+            if (status === 503)
+                this.Messages.showInfo(data);
+            else
+                this.Messages.showError('Failed to remove user: ', data);
+        });
+    }
+
+    /**
+     * @param {import('app/modules/user/User.service').User} user
+     */
+    toggleAdmin(user) {
+        const adminFlag = !user.admin;
+
+        return this.$http.post('/api/v1/admin/toggle', {
+            userId: user._id,
+            adminFlag
+        })
+        .then(() => {
+            user.admin = adminFlag;
+
+            this.Messages.showInfo(`Admin rights was successfully ${adminFlag ? 'granted' : 'revoked'} for user: "${user.userName}"`);
+        })
+        .catch((res) => {
+            this.Messages.showError(`Failed to ${adminFlag ? 'grant' : 'revoke'} admin rights for user: "${user.userName}"`, res);
+        });
+    }
+
+    /**
+     * @param {import('app/modules/user/User.service').User} user
+     */
+    prepareUsers(user) {
+        const { Countries } = this;
+
+        user.userName = user.firstName + ' ' + user.lastName;
+        user.company = user.company ? user.company.toLowerCase() : '';
+        user.lastActivity = user.lastActivity || user.lastLogin;
+        user.countryCode = Countries.getByName(user.country).code;
+
+        return user;
+    }
+
+    loadUsers(params) {
+        return this.$http.post('/api/v1/admin/list', params)
+            .then(({ data }) => data)
+            .then((users) => _.map(users, this.prepareUsers.bind(this)))
+            .catch(this.Messages.showError);
+    }
+}
diff --git a/modules/frontend/app/core/index.js b/modules/frontend/app/core/index.js
new file mode 100644
index 0000000..7f72ee3
--- /dev/null
+++ b/modules/frontend/app/core/index.js
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import IgniteAdminData from './admin/Admin.data';
+import IgniteActivitiesData from './activities/Activities.data';
+
+angular.module('ignite-console.core', [])
+    .service('IgniteAdminData', IgniteAdminData)
+    .service('IgniteActivitiesData', IgniteActivitiesData);
diff --git a/modules/frontend/app/core/utils/maskNull.js b/modules/frontend/app/core/utils/maskNull.js
new file mode 100644
index 0000000..604b690
--- /dev/null
+++ b/modules/frontend/app/core/utils/maskNull.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Filter that will check value and return `null` if needed.
+export default function(val) {
+    return _.isNil(val) ? 'null' : val;
+}
diff --git a/modules/frontend/app/data/colors.json b/modules/frontend/app/data/colors.json
new file mode 100644
index 0000000..188e485
--- /dev/null
+++ b/modules/frontend/app/data/colors.json
@@ -0,0 +1,22 @@
+[
+  "#1f77b4",
+  "#ff7f0e",
+  "#2ca02c",
+  "#d62728",
+  "#9467bd",
+  "#8c564b",
+  "#e377c2",
+  "#7f7f7f",
+  "#bcbd22",
+  "#17becf",
+  "#ffbb78",
+  "#98df8a",
+  "#ff9896",
+  "#c5b0d5",
+  "#aec7e8",
+  "#c49c94",
+  "#f7b6d2",
+  "#c7c7c7",
+  "#dbdb8d",
+  "#9edae5"
+]
diff --git a/modules/frontend/app/data/countries.json b/modules/frontend/app/data/countries.json
new file mode 100644
index 0000000..8f274ad
--- /dev/null
+++ b/modules/frontend/app/data/countries.json
@@ -0,0 +1,179 @@
+[
+  {
+    "label": "United States",
+    "value": "United States",
+    "code": "USA"
+  },
+  {
+    "label": "Canada",
+    "value": "Canada",
+    "code": "CAN"
+  },
+  {
+    "label": "United Kingdom",
+    "value": "United Kingdom",
+    "code": "GBR"
+  },
+  {
+    "label": "Germany",
+    "value": "Germany",
+    "code": "DEU"
+  },
+  {
+    "label": "France",
+    "value": "France",
+    "code": "FRA"
+  },
+  {
+    "label": "Belgium",
+    "value": "Belgium",
+    "code": "BEL"
+  },
+  {
+    "label": "Switzerland",
+    "value": "Switzerland",
+    "code": "CHE"
+  },
+  {
+    "label": "Netherlands",
+    "value": "Netherlands",
+    "code": "NLD"
+  },
+  {
+    "label": "Israel",
+    "value": "Israel",
+    "code": "ISR"
+  },
+  {
+    "label": "Sweden",
+    "value": "Sweden",
+    "code": "SWE"
+  },
+  {
+    "label": "Russia",
+    "value": "Russia",
+    "code": "RUS"
+  },
+  {
+    "label": "Other Europe",
+    "value": "Other Europe",
+    "code": "Other Europe"
+  },
+  {
+    "label": "China",
+    "value": "China",
+    "code": "CHN"
+  },
+  {
+    "label": "Japan",
+    "value": "Japan",
+    "code": "JPN"
+  },
+  {
+    "label": "South Korea",
+    "value": "South Korea",
+    "code": "KR"
+  },
+  {
+    "label": "Hong Kong",
+    "value": "Hong Kong",
+    "code": "HK"
+  },
+  {
+    "label": "Taiwan",
+    "value": "Taiwan",
+    "code": "TW"
+  },
+  {
+    "label": "Australia",
+    "value": "Australia",
+    "code": "AUS"
+  },
+  {
+    "label": "Thailand",
+    "value": "Thailand",
+    "code": "TH"
+  },
+  {
+    "label": "Singapore",
+    "value": "Singapore",
+    "code": "SG"
+  },
+  {
+    "label": "Malaysia",
+    "value": "Malaysia",
+    "code": "MY"
+  },
+  {
+    "label": "Philippines",
+    "value": "Philippines",
+    "code": "PH"
+  },
+  {
+    "label": "Indonesia",
+    "value": "Indonesia",
+    "code": "ID"
+  },
+  {
+    "label": "Vietnam",
+    "value": "Vietnam",
+    "code": "VN"
+  },
+  {
+    "label": "Macau",
+    "value": "Macau",
+    "code": "MO"
+  },
+  {
+    "label": "New Zealand",
+    "value": "New Zealand",
+    "code": "NZ"
+  },
+  {
+    "label": "India",
+    "value": "India",
+    "code": "IND"
+  },
+
+  {
+    "label": "Other Asia",
+    "value": "Other Asia",
+    "code": "Other Asia"
+  },
+
+  {
+    "label": "Brazil",
+    "value": "Brazil",
+    "code": "BRA"
+  },
+  {
+    "label": "Argentina",
+    "value": "Argentina",
+    "code": "ARG"
+  },
+  {
+    "label": "Other South America",
+    "value": "Other South America",
+    "code": "Other South America"
+  },
+  {
+    "label": "South Africa",
+    "value": "South Africa",
+    "code": "ZAF"
+  },
+  {
+    "label": "Nigeria",
+    "value": "Nigeria",
+    "code": "NGA"
+  },
+  {
+    "label": "Other Africa",
+    "value": "Other Africa",
+    "code": "Other Africa"
+  },
+  {
+    "label": "Rest of the World",
+    "value": "Rest of the World",
+    "code": "Rest of the World"
+  }
+]
diff --git a/modules/frontend/app/data/demo-info.json b/modules/frontend/app/data/demo-info.json
new file mode 100644
index 0000000..0d2ad22
--- /dev/null
+++ b/modules/frontend/app/data/demo-info.json
@@ -0,0 +1,14 @@
+[
+    {
+        "title": "Apache Ignite Web Console Demo",
+        "message": [
+            "<div>",
+            " <h4><i class='fa fa-cogs fa-cursor-default'></i>&nbsp;What Can You Do</h4>",
+            " <ul>",
+            "  <li><b>Configuration</b> to checkout predefined clusters, caches, domain models and IGFS</li>",
+            "  <li><b>SQL</b> to run various SQL queries on the demo database</li>",
+            " </ul>",
+            "</div>"
+        ]
+    }
+]
diff --git a/modules/frontend/app/data/dialects.json b/modules/frontend/app/data/dialects.json
new file mode 100644
index 0000000..007fbc6
--- /dev/null
+++ b/modules/frontend/app/data/dialects.json
@@ -0,0 +1,9 @@
+{
+  "Generic": "org.apache.ignite.cache.store.jdbc.dialect.BasicJdbcDialect",
+  "Oracle": "org.apache.ignite.cache.store.jdbc.dialect.OracleDialect",
+  "DB2": "org.apache.ignite.cache.store.jdbc.dialect.DB2Dialect",
+  "SQLServer": "org.apache.ignite.cache.store.jdbc.dialect.SQLServerDialect",
+  "MySQL": "org.apache.ignite.cache.store.jdbc.dialect.MySQLDialect",
+  "PostgreSQL": "org.apache.ignite.cache.store.jdbc.dialect.BasicJdbcDialect",
+  "H2": "org.apache.ignite.cache.store.jdbc.dialect.H2Dialect"
+}
\ No newline at end of file
diff --git a/modules/frontend/app/data/event-groups.json b/modules/frontend/app/data/event-groups.json
new file mode 100644
index 0000000..8d0c878
--- /dev/null
+++ b/modules/frontend/app/data/event-groups.json
@@ -0,0 +1,169 @@
+[
+  {
+    "label": "EVTS_CHECKPOINT",
+    "value": "EVTS_CHECKPOINT",
+    "class": "org.apache.ignite.events.EventType",
+    "events": [
+      "EVT_CHECKPOINT_SAVED",
+      "EVT_CHECKPOINT_LOADED",
+      "EVT_CHECKPOINT_REMOVED"
+    ]
+  },
+  {
+    "label": "EVTS_DEPLOYMENT",
+    "value": "EVTS_DEPLOYMENT",
+    "class": "org.apache.ignite.events.EventType",
+    "events": [
+      "EVT_CLASS_DEPLOYED",
+      "EVT_CLASS_UNDEPLOYED",
+      "EVT_CLASS_DEPLOY_FAILED",
+      "EVT_TASK_DEPLOYED",
+      "EVT_TASK_UNDEPLOYED",
+      "EVT_TASK_DEPLOY_FAILED"
+    ]
+  },
+  {
+    "label": "EVTS_ERROR",
+    "value": "EVTS_ERROR",
+    "class": "org.apache.ignite.events.EventType",
+    "events": [
+      "EVT_JOB_TIMEDOUT",
+      "EVT_JOB_FAILED",
+      "EVT_JOB_FAILED_OVER",
+      "EVT_JOB_REJECTED",
+      "EVT_JOB_CANCELLED",
+      "EVT_TASK_TIMEDOUT",
+      "EVT_TASK_FAILED",
+      "EVT_CLASS_DEPLOY_FAILED",
+      "EVT_TASK_DEPLOY_FAILED",
+      "EVT_TASK_DEPLOYED",
+      "EVT_TASK_UNDEPLOYED",
+      "EVT_CACHE_REBALANCE_STARTED",
+      "EVT_CACHE_REBALANCE_STOPPED"
+    ]
+  },
+  {
+    "label": "EVTS_DISCOVERY",
+    "value": "EVTS_DISCOVERY",
+    "class": "org.apache.ignite.events.EventType",
+    "events": [
+      "EVT_NODE_JOINED",
+      "EVT_NODE_LEFT",
+      "EVT_NODE_FAILED",
+      "EVT_NODE_SEGMENTED",
+      "EVT_CLIENT_NODE_DISCONNECTED",
+      "EVT_CLIENT_NODE_RECONNECTED"
+    ]
+  },
+  {
+    "label": "EVTS_JOB_EXECUTION",
+    "value": "EVTS_JOB_EXECUTION",
+    "class": "org.apache.ignite.events.EventType",
+    "events": [
+      "EVT_JOB_MAPPED",
+      "EVT_JOB_RESULTED",
+      "EVT_JOB_FAILED_OVER",
+      "EVT_JOB_STARTED",
+      "EVT_JOB_FINISHED",
+      "EVT_JOB_TIMEDOUT",
+      "EVT_JOB_REJECTED",
+      "EVT_JOB_FAILED",
+      "EVT_JOB_QUEUED",
+      "EVT_JOB_CANCELLED"
+    ]
+  },
+  {
+    "label": "EVTS_TASK_EXECUTION",
+    "value": "EVTS_TASK_EXECUTION",
+    "class": "org.apache.ignite.events.EventType",
+    "events": [
+      "EVT_TASK_STARTED",
+      "EVT_TASK_FINISHED",
+      "EVT_TASK_FAILED",
+      "EVT_TASK_TIMEDOUT",
+      "EVT_TASK_SESSION_ATTR_SET",
+      "EVT_TASK_REDUCED"
+    ]
+  },
+  {
+    "label": "EVTS_CACHE",
+    "value": "EVTS_CACHE",
+    "class": "org.apache.ignite.events.EventType",
+    "events": [
+      "EVT_CACHE_ENTRY_CREATED",
+      "EVT_CACHE_ENTRY_DESTROYED",
+      "EVT_CACHE_OBJECT_PUT",
+      "EVT_CACHE_OBJECT_READ",
+      "EVT_CACHE_OBJECT_REMOVED",
+      "EVT_CACHE_OBJECT_LOCKED",
+      "EVT_CACHE_OBJECT_UNLOCKED",
+      "EVT_CACHE_OBJECT_SWAPPED",
+      "EVT_CACHE_OBJECT_UNSWAPPED",
+      "EVT_CACHE_OBJECT_EXPIRED"
+    ]
+  },
+  {
+    "label": "EVTS_CACHE_REBALANCE",
+    "value": "EVTS_CACHE_REBALANCE",
+    "class": "org.apache.ignite.events.EventType",
+    "events": [
+      "EVT_CACHE_REBALANCE_STARTED",
+      "EVT_CACHE_REBALANCE_STOPPED",
+      "EVT_CACHE_REBALANCE_PART_LOADED",
+      "EVT_CACHE_REBALANCE_PART_UNLOADED",
+      "EVT_CACHE_REBALANCE_OBJECT_LOADED",
+      "EVT_CACHE_REBALANCE_OBJECT_UNLOADED",
+      "EVT_CACHE_REBALANCE_PART_DATA_LOST"
+    ]
+  },
+  {
+    "label": "EVTS_CACHE_LIFECYCLE",
+    "value": "EVTS_CACHE_LIFECYCLE",
+    "class": "org.apache.ignite.events.EventType",
+    "events": [
+      "EVT_CACHE_STARTED",
+      "EVT_CACHE_STOPPED",
+      "EVT_CACHE_NODES_LEFT"
+    ]
+  },
+  {
+    "label": "EVTS_CACHE_QUERY",
+    "value": "EVTS_CACHE_QUERY",
+    "class": "org.apache.ignite.events.EventType",
+    "events": [
+      "EVT_CACHE_QUERY_EXECUTED",
+      "EVT_CACHE_QUERY_OBJECT_READ"
+    ]
+  },
+  {
+    "label": "EVTS_SWAPSPACE",
+    "value": "EVTS_SWAPSPACE",
+    "class": "org.apache.ignite.events.EventType",
+    "events": [
+      "EVT_SWAP_SPACE_CLEARED",
+      "EVT_SWAP_SPACE_DATA_REMOVED",
+      "EVT_SWAP_SPACE_DATA_READ",
+      "EVT_SWAP_SPACE_DATA_STORED",
+      "EVT_SWAP_SPACE_DATA_EVICTED"
+    ]
+  },
+  {
+    "label": "EVTS_IGFS",
+    "value": "EVTS_IGFS",
+    "class": "org.apache.ignite.events.EventType",
+    "events": [
+      "EVT_IGFS_FILE_CREATED",
+      "EVT_IGFS_FILE_RENAMED",
+      "EVT_IGFS_FILE_DELETED",
+      "EVT_IGFS_FILE_OPENED_READ",
+      "EVT_IGFS_FILE_OPENED_WRITE",
+      "EVT_IGFS_FILE_CLOSED_WRITE",
+      "EVT_IGFS_FILE_CLOSED_READ",
+      "EVT_IGFS_FILE_PURGED",
+      "EVT_IGFS_META_UPDATED",
+      "EVT_IGFS_DIR_CREATED",
+      "EVT_IGFS_DIR_RENAMED",
+      "EVT_IGFS_DIR_DELETED"
+    ]
+  }
+]
diff --git a/modules/frontend/app/data/getting-started.json b/modules/frontend/app/data/getting-started.json
new file mode 100644
index 0000000..58ca367
--- /dev/null
+++ b/modules/frontend/app/data/getting-started.json
@@ -0,0 +1,129 @@
+[
+    {
+        "title": "With Apache Ignite Web Console You Can",
+        "message": [
+            "<div class='col-40 align-center'>",
+            " <img src='/images/ignite-puzzle.png' width='170px' class='getting-started-puzzle' />",
+            "</div>",
+            "<div class='col-60'>",
+            " <ul class='align-top'>",
+            "  <li>Generate cluster configuration</li>",
+            "  <li>Import domain model from database</li>",
+            "  <li>Configure all needed caches</li>",
+            "  <li>Preview generated XML and Java code in browser</li>",
+            "  <li>Download ready-to-use Maven project</li>",
+            "  <li>Execute SQL queries on real clusters</li>",
+            " </ul>",
+            "</div>"
+        ]
+    },
+    {
+        "title": "Quick cluster configuration",
+        "message": [
+            "<div class='col-60'>",
+            " <img src='/images/cluster-quick.png' width='100%' />",
+            "</div>",
+            "<div class='col-40'>",
+            " <ul class='align-top'>",
+            "  <li>Quick configuration of cluster and it's caches</li>",
+            " </ul>",
+            "</div>"
+        ]
+    },
+    {
+        "title": "Clusters",
+        "message": [
+            "<div class='col-60'>",
+            " <img src='/images/cluster.png' width='100%' />",
+            "</div>",
+            "<div class='col-40'>",
+            " <ul class='align-top'>",
+            "  <li>Configure cluster properties</li>",
+            " </ul>",
+            "</div>"
+        ]
+    },
+    {
+        "title": "Domain Model",
+        "message": [
+            "<div class='col-60'>",
+            " <img src='/images/domains.png' width='100%' />",
+            "</div>",
+            "<div class='col-40'>",
+            " <ul class='align-top'>",
+            "  <li>Import database schemas</li>",
+            "  <li>Try in <span class='getting-started-demo'>Demo</span> mode</li>",
+            " </ul>",
+            "</div>"
+        ]
+    },
+    {
+        "title": "Caches",
+        "message": [
+            "<div class='col-60'>",
+            " <img src='/images/cache.png' width='100%' />",
+            "</div>",
+            "<div class='col-40'>",
+            " <ul class='align-top'>",
+            "  <li>Configure memory settings</li>",
+            " </ul>",
+            "</div>"
+        ]
+    },
+    {
+        "title": "In-memory File System",
+        "message": [
+            "<div class='col-60'>",
+            " <img src='/images/igfs.png' width='100%' />",
+            "</div>",
+            "<div class='col-40'>",
+            " <ul class='align-top'>",
+            "  <li>Configure IGFS properties</li>",
+            " </ul>",
+            "</div>"
+        ]
+    },
+    {
+        "title": "Preview configuration result",
+        "message": [
+            "<div class='col-60'>",
+            " <img src='/images/preview.png' width='100%' />",
+            "</div>",
+            "<div class='col-40'>",
+            " <ul class='align-top'>",
+            "  <li>Preview configured project files</li>",
+            "  <li>Download configured project files</li>",
+            " </ul>",
+            "</div>"
+        ]
+    },
+    {
+        "title": "SQL Queries",
+        "message": [
+            "<div class='col-60'>",
+            " <img src='/images/query-table.png' width='100%' />",
+            "</div>",
+            "<div class='col-40'>",
+            " <ul class='align-top'>",
+            "  <li>Execute SQL Queries</li>",
+            "  <li>View Execution Paln</li>",
+            "  <li>View In-Memory Schema</li>",
+            "  <li>View Streaming Charts</li>",
+            " </ul>",
+            "</div>"
+        ]
+    },
+    {
+        "title": "Multicluster support",
+        "message": [
+            "<div class='col-60'>",
+            " <img src='/images/multicluster.png' width='100%' />",
+            "</div>",
+            "<div class='col-40'>",
+            " <ul class='align-top'>",
+            "  <li>Execute queries on different clusters</li>",
+            " </ul>",
+            "</div>"
+        ]
+    }
+]
diff --git a/modules/frontend/app/data/i18n.js b/modules/frontend/app/data/i18n.js
new file mode 100644
index 0000000..20f9d25
--- /dev/null
+++ b/modules/frontend/app/data/i18n.js
@@ -0,0 +1,308 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default {
+    '/agent/start': 'Agent start',
+    '/agent/download': 'Agent download',
+    '/configuration/overview': 'Cluster configurations',
+    '/configuration/new': 'Cluster configuration create',
+    '/configuration/new/basic': 'Basic cluster configuration create',
+    '/configuration/new/advanced/cluster': 'Advanced cluster configuration create',
+    '/configuration/download': 'Download project',
+    'configuration/import/model': 'Import cluster models',
+    '/demo/resume': 'Demo resume',
+    '/demo/reset': 'Demo reset',
+    '/queries/execute': 'Query execute',
+    '/queries/explain': 'Query explain',
+    '/queries/scan': 'Scan',
+    '/queries/add/query': 'Add query',
+    '/queries/add/scan': 'Add scan',
+    '/queries/demo': 'SQL demo',
+    '/queries/notebook/': 'Query notebook',
+    '/settings/profile': 'User profile',
+    '/settings/admin': 'Admin panel',
+    '/logout': 'Logout',
+
+    'base.configuration.overview': 'Cluster configurations',
+    'base.configuration.edit.basic': 'Basic cluster configuration edit',
+    'base.configuration.edit.advanced.cluster': 'Advanced cluster configuration edit',
+    'base.configuration.edit.advanced.caches': 'Advanced cluster caches',
+    'base.configuration.edit.advanced.caches.cache': 'Advanced cluster cache edit',
+    'base.configuration.edit.advanced.models': 'Advanced cluster models',
+    'base.configuration.edit.advanced.models.model': 'Advanced cluster model edit',
+    'base.configuration.edit.advanced.igfs': 'Advanced cluster IGFSs',
+    'base.configuration.edit.advanced.igfs.igfs': 'Advanced cluster IGFS edit',
+    'base.settings.admin': 'Admin panel',
+    'base.settings.profile': 'User profile',
+    'base.sql.notebook': 'Query notebook',
+    'base.sql.tabs.notebooks-list': 'Query notebooks',
+
+    // app/components/page-signin/template.pug
+    'app.components.page-signin.m1': 'Sign In',
+    'app.components.page-signin.m2': 'Email:',
+    'app.components.page-signin.m3': 'Input email',
+    'app.components.page-signin.m4': 'Password:',
+    'app.components.page-signin.m5': 'Input password',
+    'app.components.page-signin.m6': 'Forgot password?',
+    'app.components.page-signin.m7': 'Sign In',
+    'app.components.page-signin.m8': 'Don\'t have an account? #[a(ui-sref=\'signup\') Get started]',
+
+    // app/components/page-signin/run.js
+    'app.components.page-signin.m9': 'Sign In',
+
+
+    // app/components/page-queries/template.tpl.pug
+    'app.components.page-queries.m1': 'Show data in tabular form',
+    'app.components.page-queries.m2': 'Show bar chart<br/>By default first column - X values, second column - Y values<br/>In case of one column it will be treated as Y values',
+    'app.components.page-queries.m3': 'Show pie chart<br/>By default first column - pie labels, second column - pie values<br/>In case of one column it will be treated as pie values',
+    'app.components.page-queries.m4': 'Show line chart<br/>By default first column - X values, second column - Y values<br/>In case of one column it will be treated as Y values',
+    'app.components.page-queries.m5': 'Show area chart<br/>By default first column - X values, second column - Y values<br/>In case of one column it will be treated as Y values',
+    'app.components.page-queries.m6': 'Click to show chart settings dialog',
+    'app.components.page-queries.m7': 'Chart settings',
+    'app.components.page-queries.m8': 'Show',
+    'app.components.page-queries.m9': 'min',
+    'app.components.page-queries.m10': 'Duration: #[b {{paragraph.duration | duration}}]',
+    'app.components.page-queries.m11': 'NodeID8: #[b {{paragraph.resNodeId | id8}}]',
+    'app.components.page-queries.m12': 'Rename notebook',
+    'app.components.page-queries.m13': 'Remove notebook',
+    'app.components.page-queries.m14': 'Save notebook name',
+    'app.components.page-queries.m15': 'Scroll to query',
+    'app.components.page-queries.m16': 'Add query',
+    'app.components.page-queries.m17': 'Add scan',
+    'app.components.page-queries.m18': 'Failed to load notebook',
+    'app.components.page-queries.m19': 'Notebook not accessible any more. Leave notebooks or open another notebook.',
+    'app.components.page-queries.m20': 'Leave notebooks',
+    'app.components.page-queries.m21': 'Rename query',
+    'app.components.page-queries.m22': 'Rename query',
+    'app.components.page-queries.m23': 'Remove query',
+    'app.components.page-queries.m24': 'Save query name',
+    'app.components.page-queries.m25': 'Configure periodical execution of last successfully executed query',
+    'app.components.page-queries.m26': 'Refresh rate:',
+    'app.components.page-queries.m27': 'Max number of rows to show in query result as one page',
+    'app.components.page-queries.m28': 'Page size:',
+    'app.components.page-queries.m29': 'Limit query max results to specified number of pages',
+    'app.components.page-queries.m30': 'Max pages:',
+    'app.components.page-queries.m31': 'Non-collocated joins is a special mode that allow to join data across cluster without collocation.<br/>Nested joins are not supported for now.<br/><b>NOTE</b>: In some cases it may consume more heap memory or may take a long time than collocated joins.',
+    'app.components.page-queries.m32': 'Allow non-collocated joins',
+    'app.components.page-queries.m33': 'Enforce join order of tables in the query.<br/>If <b>set</b>, then query optimizer will not reorder tables within join.<br/><b>NOTE:</b> It is not recommended to enable this property unless you have verified that indexes are not selected in optimal order.',
+    'app.components.page-queries.m34': 'Enforce join order',
+    'app.components.page-queries.m35': 'By default Ignite attempts to fetch the whole query result set to memory and send it to the client.<br/>For small and medium result sets this provides optimal performance and minimize duration of internal database locks, thus increasing concurrency.<br/>If result set is too big to fit in available memory this could lead to excessive GC pauses and even OutOfMemoryError.<br/>Use this flag as a hint for Ignite to fetch result set lazily, thus minimizing memory consumption at the cost of moderate performance hit.',
+    'app.components.page-queries.m36': 'Lazy result set',
+    'app.components.page-queries.m37': 'Execute',
+    'app.components.page-queries.m38': 'Execute on selected node',
+    'app.components.page-queries.m39': '{{queryTooltip(paragraph, "explain query")}}',
+    'app.components.page-queries.m40': 'Explain',
+    'app.components.page-queries.m41': 'Page: #[b {{paragraph.page}}]',
+    'app.components.page-queries.m42': 'Results so far: #[b {{paragraph.rows.length + paragraph.total}}]',
+    'app.components.page-queries.m43': 'Duration: #[b {{paragraph.duration | duration}}]',
+    'app.components.page-queries.m44': 'NodeID8: #[b {{paragraph.resNodeId | id8}}]',
+    'app.components.page-queries.m45': '{{ queryTooltip(paragraph, "export query results") }}',
+    'app.components.page-queries.m46': 'Export',
+    'app.components.page-queries.m47': 'Export',
+    'app.components.page-queries.m48': 'Export all',
+    'app.components.page-queries.m49': 'Copy current result page to clipboard',
+    'app.components.page-queries.m50': 'Copy to clipboard',
+    'app.components.page-queries.m51': 'Page: #[b {{paragraph.pa',
+    'app.components.page-queries.m52': 'Results so far: #[b {{paragraph.rows.length + paragraph.total}}]',
+    'app.components.page-queries.m53': 'Duration: #[b {{paragraph.duration | duration}}]',
+    'app.components.page-queries.m54': 'NodeID8: #[b {{paragraph.resNodeId | id8}}]',
+    'app.components.page-queries.m55': 'Export',
+    'app.components.page-queries.m56': 'Export',
+    'app.components.page-queries.m57': 'Export all',
+    'app.components.page-queries.m58': 'Copy current result page to clipboard',
+    'app.components.page-queries.m59': 'Copy to clipboard',
+    'app.components.page-queries.m60': 'Cannot display chart. Please configure axis using #[b Chart settings]',
+    'app.components.page-queries.m61': 'Cannot display chart. Result set must contain Java build-in type columns. Please change query and execute it again.',
+    'app.components.page-queries.m62': 'Pie chart does not support \'TIME_LINE\' column for X-axis. Please use another column for X-axis or switch to another chart.',
+    'app.components.page-queries.m63': 'Charts do not support #[b Explain] and #[b Scan] query',
+    'app.components.page-queries.m64': 'Cache:',
+    'app.components.page-queries.m65': 'Choose cache',
+    'app.components.page-queries.m66': 'Filter:',
+    'app.components.page-queries.m67': 'Enter filter',
+    'app.components.page-queries.m68': 'Select this checkbox for case sensitive search',
+    'app.components.page-queries.m69': 'Max number of rows to show in query result as one page',
+    'app.components.page-queries.m70': 'Page size:',
+    'app.components.page-queries.m71': 'Scan',
+    'app.components.page-queries.m72': 'Scan on selected node',
+    'app.components.page-queries.m73': 'Error: {{paragraph.error.message}}',
+    'app.components.page-queries.m74': 'Result set is empty. Duration: #[b {{paragraph.duration | duration}}]',
+    'app.components.page-queries.m75': 'Showing results for scan of #[b {{ paragraph.queryArgs.cacheName | defaultName }}]',
+    'app.components.page-queries.m76': '&nbsp; with filter: #[b {{ paragraph.queryArgs.filter }}]',
+    'app.components.page-queries.m77': '&nbsp; on node: #[b {{ paragraph.queryArgs.localNid | limitTo:8 }}]',
+    'app.components.page-queries.m78': 'Next',
+    'app.components.page-queries.m79': 'Caches:',
+    'app.components.page-queries.m80': 'Click to show cache types metadata dialog',
+    'app.components.page-queries.m81': 'Filter caches...',
+    'app.components.page-queries.m82': 'Use selected cache as default schema name.<br/>This will allow to execute query on specified cache without specify schema name.<br/><b>NOTE:</b> In future version of Ignite this feature will be removed.',
+    'app.components.page-queries.m83': 'Use selected cache as default schema name',
+    'app.components.page-queries.m84': 'Wrong caches filter',
+    'app.components.page-queries.m85': 'No caches',
+    'app.components.page-queries.m86': 'Error: {{paragraph.error.message}}',
+    'app.components.page-queries.m87': 'Show more',
+    'app.components.page-queries.m88': 'Show query',
+    'app.components.page-queries.m89': 'Next',
+    'app.components.page-queries.m90': 'Queries',
+    'app.components.page-queries.m91': 'With query notebook you can',
+    'app.components.page-queries.m92': 'Create any number of queries',
+    'app.components.page-queries.m93': 'Execute and explain SQL queries',
+    'app.components.page-queries.m94': 'Execute scan queries',
+    'app.components.page-queries.m95': 'View data in tabular form and as charts',
+    'app.components.page-queries.m96': 'Examples:',
+    // app/components/page-queries/Notebook.service.js
+    'app.components.page-queries.m97': 'Are you sure you want to remove notebook: "${notebook.name}"?',
+    // app/components/page-queries/Notebook.data.js
+    'app.components.page-queries.m98': 'SQL demo',
+    'app.components.page-queries.m99': 'Query with refresh rate',
+    'app.components.page-queries.m100': 'Simple query',
+    'app.components.page-queries.m101': 'Query with aggregates',
+    'app.components.page-queries.m102': 'Failed to load notebook.',
+    'app.components.page-queries.m103': 'Removing "${notebook.name}" notebook is not supported.',
+    // app/components/page-queries/index.js
+    'app.components.page-queries.m104': 'Query notebook',
+    'app.components.page-queries.m105': 'SQL demo',
+    // app/components/page-queries/controller.js
+    'app.components.page-queries.m106': 'Internal cluster error',
+    'app.components.page-queries.m107': 'Unlimited',
+    'app.components.page-queries.m108': 'Demo grid is starting. Please wait...',
+    'app.components.page-queries.m109': 'Loading query notebook screen...',
+    'app.components.page-queries.m110': 'seconds',
+    'app.components.page-queries.m111': 's',
+    'app.components.page-queries.m112': 'minutes',
+    'app.components.page-queries.m113': 'm',
+    'app.components.page-queries.m114': 'hours',
+    'app.components.page-queries.m115': 'h',
+    'app.components.page-queries.m116': 'Leave Queries',
+    'app.components.page-queries.m117': 'Query ${sz === 0 ? "" : sz}',
+    'app.components.page-queries.m118': 'Scan ${sz === 0 ? "" : sz}',
+    'app.components.page-queries.m119': 'Are you sure you want to remove query: "${paragraph.name}"?',
+    'app.components.page-queries.m120': 'Waiting for server response',
+    'app.components.page-queries.m121': 'Input text to ${action}',
+    'app.components.page-queries.m122': 'Waiting for server response',
+    'app.components.page-queries.m123': 'Select cache to export scan results',
+    'app.components.page-queries.m124': 'SCAN query',
+    'app.components.page-queries.m125': 'SCAN query for cache: <b>${maskCacheName(paragraph.queryArgs.cacheName, true)}</b>',
+    'app.components.page-queries.m126': 'SCAN query for cache: <b>${maskCacheName(paragraph.queryArgs.cacheName, true)}</b> with filter: <b>${filter}</b>',
+    'app.components.page-queries.m127': 'Explain query',
+    'app.components.page-queries.m128': 'SQL query',
+    'app.components.page-queries.m129': 'Duration: ${$filter(\'duration\')(paragraph.duration)}.',
+    'app.components.page-queries.m130': 'Node ID8: ${_.id8(paragraph.resNodeId)}',
+    'app.components.page-queries.m131': 'Error details',
+
+    // app/components/page-queries/services/queries-navbar.js
+    'app.components.page-queries.services.queries-navbar.m1': 'Queries',
+    'app.components.page-queries.services.queries-navbar.m2': 'Create new notebook',
+
+    // app/components/page-queries/services/create-query-dialog/template.pug
+    'app.components.page-queries.services.create-query-dialog.m1': 'New query notebook',
+    'app.components.page-queries.services.create-query-dialog.m2': 'Name:&nbsp;',
+    'app.components.page-queries.services.create-query-dialog.m3': 'Cancel',
+    'app.components.page-queries.services.create-query-dialog.m4': 'Create',
+
+    // app/components/page-profile/controller.js
+    'app.components.page-profile.m1': 'Are you sure you want to change security token?',
+    'app.components.page-profile.m2': 'Profile saved.',
+    'app.components.page-profile.m3': 'Failed to save profile: ',
+    // app/components/page-profile/index.js
+    'app.components.page-profile.m4': 'User profile',
+    // app/components/page-profile/template.pug
+    'app.components.page-profile.m5': 'User profile',
+    'app.components.page-profile.m6': 'First name:',
+    'app.components.page-profile.m7': 'Input first name',
+    'app.components.page-profile.m8': 'Last name:',
+    'app.components.page-profile.m9': 'Input last name',
+    'app.components.page-profile.m10': 'Email:',
+    'app.components.page-profile.m11': 'Input email',
+    'app.components.page-profile.m12': 'Phone:',
+    'app.components.page-profile.m13': 'Input phone (ex.: +15417543010)',
+    'app.components.page-profile.m14': 'Country:',
+    'app.components.page-profile.m15': 'Choose your country',
+    'app.components.page-profile.m16': 'Company:',
+    'app.components.page-profile.m17': 'Input company name',
+    'app.components.page-profile.m18': 'Cancel security token changing...',
+    'app.components.page-profile.m19': 'Show security token...',
+    'app.components.page-profile.m20': 'Security token:',
+    'app.components.page-profile.m21': 'No security token. Regenerate please.',
+    'app.components.page-profile.m22': 'Generate random security token',
+    'app.components.page-profile.m23': 'Copy security token to clipboard',
+    'app.components.page-profile.m24': 'The security token is used for authorization of web agent',
+    'app.components.page-profile.m25': 'Cancel password changing...',
+    'app.components.page-profile.m26': 'Change password...',
+    'app.components.page-profile.m27': 'New password:',
+    'app.components.page-profile.m28': 'New password',
+    'app.components.page-profile.m29': 'Confirm password:',
+    'app.components.page-profile.m30': 'Confirm new password',
+    'app.components.page-profile.m31': 'Cancel',
+    'app.components.page-profile.m32': 'Save Changes',
+
+    // app/modules/navbar/userbar.directive.js
+    'app/modules/navbar/userbar.m1': 'Profile',
+    'app/modules/navbar/userbar.m2': 'Getting started',
+    'app/modules/navbar/userbar.m3': 'Admin panel',
+    'app/modules/navbar/userbar.m4': 'Log out',
+
+    // app/components/page-forgot-password/run.js
+    'app.components.page-forgot-password.m1': 'Forgot Password',
+    // app/components/page-forgot-password/template.pug
+    'app.components.page-forgot-password.m2': 'Forgot password?',
+    'app.components.page-forgot-password.m3': 'Enter the email address for your account & we\'ll email you a link to reset your password.',
+    'app.components.page-forgot-password.m4': 'Email:',
+    'app.components.page-forgot-password.m5': 'Input email',
+    'app.components.page-forgot-password.m6': 'Back to sign in',
+    'app.components.page-forgot-password.m7': 'Send it to me',
+
+    // app/components/page-landing/template.pug
+    'app.components.page-landing.m1': 'Sign In',
+    'app.components.page-landing.m2': 'Web Console',
+    'app.components.page-landing.m3': 'An Interactive Configuration Wizard and Management Tool for Apache™ Ignite®',
+    'app.components.page-landing.m4': 'It provides an interactive configuration wizard which helps you create and download configuration files and code snippets for your Apache Ignite projects. Additionally, the tool allows you to automatically load SQL metadata from any RDBMS, run SQL queries on your in-memory cache, and view execution plans, in-memory schema, and streaming charts.',
+    'app.components.page-landing.m5': 'Sign Up',
+    'app.components.page-landing.m6': 'The Web Console allows you to:',
+    'app.components.page-landing.m7': 'Configure Apache Ignite clusters and caches',
+    'app.components.page-landing.m8': 'The Web Console configuration wizard takes you through a step-by-step process that helps you define all the required configuration parameters. The system then generates a ready-to-use project with all the required config files.',
+    'app.components.page-landing.m9': 'Run free-form SQL queries on #[br] Apache Ignite caches',
+    'app.components.page-landing.m10': 'By connecting the Web Console to your Apache Ignite cluster, you can execute SQL queries on your in-memory cache. You can also view the execution plan, in-memory schema, and streaming charts for your cluster.',
+    'app.components.page-landing.m11': 'Import database schemas from #[br] virtually any RDBMS',
+    'app.components.page-landing.m12': 'To speed the creation of your configuration files, the Web Console allows you to automatically import the database schema from virtually any RDBMS including Oracle, SAP, MySQL, PostgreSQL, and many more.',
+    'app.components.page-landing.m13': 'Manage the Web Console users',
+    'app.components.page-landing.m14': 'The Web Console allows you to have accounts with different roles.',
+    'app.components.page-landing.m15': 'Get Started',
+
+    // app/components/page-signup/template.pug
+    'app.components.page-signup.m1': 'Don\'t Have An Account?',
+    'app.components.page-signup.m2': 'Email:',
+    'app.components.page-signup.m3': 'Input email',
+    'app.components.page-signup.m4': 'Password:',
+    'app.components.page-signup.m5': 'Input password',
+    'app.components.page-signup.m6': 'Confirm:',
+    'app.components.page-signup.m7': 'Confirm password',
+    'app.components.page-signup.m8': 'First name:',
+    'app.components.page-signup.m9': 'Input first name',
+    'app.components.page-signup.m10': 'Last name:',
+    'app.components.page-signup.m11': 'Input last name',
+    'app.components.page-signup.m12': 'Phone:',
+    'app.components.page-signup.m13': 'Input phone (ex.: +15417543010)',
+    'app.components.page-signup.m14': 'Country:',
+    'app.components.page-signup.m15': 'Choose your country',
+    'app.components.page-signup.m16': 'Company:',
+    'app.components.page-signup.m17': 'Input company name',
+    'app.components.page-signup.m18': 'Sign Up',
+    'app.components.page-signup.m19': 'Already have an account? #[a(ui-sref=\'signin\') Sign in here]',
+
+    // app/components/password-visibility/toggle-button.component.js
+    'app.components.password-visibility.m1': 'Hide password',
+    'app.components.password-visibility.m2': 'Show password'
+};
diff --git a/modules/frontend/app/data/java-classes.json b/modules/frontend/app/data/java-classes.json
new file mode 100644
index 0000000..6704457
--- /dev/null
+++ b/modules/frontend/app/data/java-classes.json
@@ -0,0 +1,21 @@
+[
+  {"short": "BigDecimal", "full": "java.math.BigDecimal"},
+  {"short": "Boolean", "full": "java.lang.Boolean"},
+  {"short": "Byte", "full": "java.lang.Byte"},
+  {"short": "Character", "full": "java.lang.Character"},
+  {"short": "Date", "full": "java.sql.Date"},
+  {"short": "java.util.Date", "full": "java.util.Date"},
+  {"short": "Double", "full": "java.lang.Double"},
+  {"short": "Float", "full": "java.lang.Float"},
+  {"short": "Integer", "full": "java.lang.Integer"},
+  {"short": "Long", "full": "java.lang.Long"},
+  {"short": "Number", "full": "java.lang.Number"},
+  {"short": "Object", "full": "java.lang.Object"},
+  {"short": "Short", "full": "java.lang.Short"},
+  {"short": "String", "full": "java.lang.String"},
+  {"short": "Time", "full": "java.sql.Time"},
+  {"short": "Timestamp", "full": "java.sql.Timestamp"},
+  {"short": "UUID", "full": "java.util.UUID"},
+  {"short": "Serializable", "full": "java.io.Serializable"},
+  {"short": "Class", "full": "java.lang.Class"}
+]
diff --git a/modules/frontend/app/data/java-keywords.json b/modules/frontend/app/data/java-keywords.json
new file mode 100644
index 0000000..a2d5ec2
--- /dev/null
+++ b/modules/frontend/app/data/java-keywords.json
@@ -0,0 +1,55 @@
+[
+  "abstract",
+  "assert",
+  "boolean",
+  "break",
+  "byte",
+  "case",
+  "catch",
+  "char",
+  "class",
+  "const",
+  "continue",
+  "default",
+  "do",
+  "double",
+  "else",
+  "enum",
+  "extends",
+  "false",
+  "final",
+  "finally",
+  "float",
+  "for",
+  "goto",
+  "if",
+  "implements",
+  "import",
+  "instanceof",
+  "int",
+  "interface",
+  "long",
+  "native",
+  "new",
+  "null",
+  "package",
+  "private",
+  "protected",
+  "public",
+  "return",
+  "short",
+  "static",
+  "strictfp",
+  "super",
+  "switch",
+  "synchronized",
+  "this",
+  "throw",
+  "throws",
+  "transient",
+  "true",
+  "try",
+  "void",
+  "volatile",
+  "while"
+]
diff --git a/modules/frontend/app/data/java-primitives.json b/modules/frontend/app/data/java-primitives.json
new file mode 100644
index 0000000..eab6b69
--- /dev/null
+++ b/modules/frontend/app/data/java-primitives.json
@@ -0,0 +1,9 @@
+[
+  "boolean",
+  "byte",
+  "double",
+  "float",
+  "int",
+  "long",
+  "short"
+]
diff --git a/modules/frontend/app/data/jdbc-types.json b/modules/frontend/app/data/jdbc-types.json
new file mode 100644
index 0000000..c743e31
--- /dev/null
+++ b/modules/frontend/app/data/jdbc-types.json
@@ -0,0 +1,44 @@
+[
+    {"dbName": "BIT", "dbType": -7, "signed": {"javaType": "Boolean", "primitiveType": "boolean"}},
+    {"dbName": "TINYINT", "dbType": -6,
+        "signed": {"javaType": "Byte", "primitiveType": "byte"},
+        "unsigned": {"javaType": "Short", "primitiveType": "short"}},
+    {"dbName": "SMALLINT", "dbType": 5,
+        "signed": {"javaType": "Short", "primitiveType": "short"},
+        "unsigned": {"javaType": "Integer", "primitiveType": "int"}},
+    {"dbName": "INTEGER", "dbType": 4,
+        "signed": {"javaType": "Integer", "primitiveType": "int"},
+        "unsigned": {"javaType": "Long", "primitiveType": "long"}},
+    {"dbName": "BIGINT", "dbType": -5, "signed": {"javaType": "Long", "primitiveType": "long"}},
+    {"dbName": "FLOAT", "dbType": 6, "signed": {"javaType": "Float", "primitiveType": "float"}},
+    {"dbName": "REAL", "dbType": 7, "signed": {"javaType": "Double", "primitiveType": "double"}},
+    {"dbName": "DOUBLE", "dbType": 8, "signed": {"javaType": "Double", "primitiveType": "double"}},
+    {"dbName": "NUMERIC", "dbType": 2, "signed": {"javaType": "BigDecimal"}},
+    {"dbName": "DECIMAL", "dbType": 3, "signed": {"javaType": "BigDecimal"}},
+    {"dbName": "CHAR", "dbType": 1, "signed": {"javaType": "String"}},
+    {"dbName": "VARCHAR", "dbType": 12, "signed": {"javaType": "String"}},
+    {"dbName": "LONGVARCHAR", "dbType": -1, "signed": {"javaType": "String"}},
+    {"dbName": "DATE", "dbType": 91, "signed": {"javaType": "Date"}},
+    {"dbName": "TIME", "dbType": 92, "signed": {"javaType": "Time"}},
+    {"dbName": "TIMESTAMP", "dbType": 93, "signed": {"javaType": "Timestamp"}},
+    {"dbName": "BINARY", "dbType": -2, "signed": {"javaType": "byte[]"}},
+    {"dbName": "VARBINARY", "dbType": -3, "signed": {"javaType": "byte[]"}},
+    {"dbName": "LONGVARBINARY", "dbType": -4, "signed": {"javaType": "byte[]"}},
+    {"dbName": "NULL", "dbType": 0, "signed": {"javaType": "Object"}},
+    {"dbName": "OTHER", "dbType": 1111, "signed": {"javaType": "Object"}},
+    {"dbName": "JAVA_OBJECT", "dbType": 2000, "signed": {"javaType": "Object"}},
+    {"dbName": "DISTINCT", "dbType": 2001, "signed": {"javaType": "Object"}},
+    {"dbName": "STRUCT", "dbType": 2002, "signed": {"javaType": "Object"}},
+    {"dbName": "ARRAY", "dbType": 2003, "signed": {"javaType": "Object"}},
+    {"dbName": "BLOB", "dbType": 2004, "signed": {"javaType": "Object"}},
+    {"dbName": "CLOB", "dbType": 2005, "signed": {"javaType": "String"}},
+    {"dbName": "REF", "dbType": 2006, "signed": {"javaType": "Object"}},
+    {"dbName": "DATALINK", "dbType": 70, "signed": {"javaType": "Object"}},
+    {"dbName": "BOOLEAN", "dbType": 16, "signed": {"javaType": "Boolean", "primitiveType": "boolean"}},
+    {"dbName": "ROWID", "dbType": -8, "signed": {"javaType": "Object"}},
+    {"dbName": "NCHAR", "dbType": -15, "signed": {"javaType": "String"}},
+    {"dbName": "NVARCHAR", "dbType": -9, "signed": {"javaType": "String"}},
+    {"dbName": "LONGNVARCHAR", "dbType": -16, "signed": {"javaType": "String"}},
+    {"dbName": "NCLOB", "dbType": 2011, "signed": {"javaType": "String"}},
+    {"dbName": "SQLXML", "dbType": 2009, "signed": {"javaType": "Object"}}
+]
diff --git a/modules/frontend/app/data/pom-dependencies.json b/modules/frontend/app/data/pom-dependencies.json
new file mode 100644
index 0000000..428455f
--- /dev/null
+++ b/modules/frontend/app/data/pom-dependencies.json
@@ -0,0 +1,28 @@
+{
+    "Cloud": {"artifactId": "ignite-cloud"},
+    "S3": {"artifactId": "ignite-aws"},
+    "GoogleStorage": {"artifactId": "ignite-gce"},
+    "ZooKeeper": {"artifactId": "ignite-zookeeper"},
+    "Kubernetes": {"artifactId": "ignite-kubernetes"},
+
+    "Log4j": {"artifactId": "ignite-log4j"},
+    "Log4j2": {"artifactId": "ignite-log4j2"},
+    "JCL": {"artifactId": "ignite-jcl"},
+    "HadoopIgfsJcl": {"artifactId": "ignite-hadoop"},
+    "SLF4J": {"artifactId": "ignite-slf4j"},
+
+    "Generic": [
+        {"groupId": "com.mchange", "artifactId": "c3p0", "version": "0.9.5.2"},
+        {"groupId": "com.mchange", "artifactId": "mchange-commons-java", "version": "0.2.11"}
+    ],
+    "MySQL": {"groupId": "mysql", "artifactId": "mysql-connector-java", "version": "8.0.15"},
+    "PostgreSQL": {"groupId": "org.postgresql", "artifactId": "postgresql", "version": "42.2.5"},
+    "H2": {"groupId": "com.h2database", "artifactId": "h2", "version": [
+        {"range": ["1.0.0", "2.0.0"], "version": "1.4.191"},
+        {"range": ["2.0.0", "2.7.0"], "version": "1.4.195"},
+        {"range": "2.7.0", "version": "1.4.197"}
+    ]},
+    "Oracle": {"groupId": "com.oracle.jdbc", "artifactId": "ojdbc8", "version": "18.3.0.0.0", "jar": "ojdbc8.jar", "link": "https://www.oracle.com/technetwork/database/application-development/jdbc/downloads/index.html"},
+    "DB2": {"groupId": "ibm", "artifactId": "jdbc", "version": "4.25.13", "jar": "db2jcc4.jar", "link": "http://www-01.ibm.com/support/docview.wss?uid=swg21363866"},
+    "SQLServer": {"groupId": "com.microsoft.sqlserver", "artifactId": "mssql-jdbc", "version": "7.2.1.jre8"}
+}
diff --git a/modules/frontend/app/data/sql-keywords.json b/modules/frontend/app/data/sql-keywords.json
new file mode 100644
index 0000000..00f4eeb
--- /dev/null
+++ b/modules/frontend/app/data/sql-keywords.json
@@ -0,0 +1,41 @@
+[
+  "CROSS",
+  "CURRENT_DATE",
+  "CURRENT_TIME",
+  "CURRENT_TIMESTAMP",
+  "DISTINCT",
+  "EXCEPT",
+  "EXISTS",
+  "FALSE",
+  "FETCH",
+  "FOR",
+  "FROM",
+  "FULL",
+  "GROUP",
+  "HAVING",
+  "INNER",
+  "INTERSECT",
+  "IS",
+  "JOIN",
+  "LIKE",
+  "LIMIT",
+  "MINUS",
+  "NATURAL",
+  "NOT",
+  "NULL",
+  "OFFSET",
+  "ON",
+  "ORDER",
+  "PRIMARY",
+  "ROWNUM",
+  "SELECT",
+  "SYSDATE",
+  "SYSTIMESTAMP",
+  "SYSTIME",
+  "TODAY",
+  "TRUE",
+  "UNION",
+  "UNIQUE",
+  "WHERE",
+  "WITH"
+]
diff --git a/modules/frontend/app/directives/auto-focus.directive.js b/modules/frontend/app/directives/auto-focus.directive.js
new file mode 100644
index 0000000..e67b50d
--- /dev/null
+++ b/modules/frontend/app/directives/auto-focus.directive.js
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Directive to auto-focus specified element.
+ * @param {ng.ITimeoutService} $timeout
+ */
+export default function directive($timeout) {
+    return {
+        restrict: 'AC',
+        /**
+         * @param {ng.IScope} scope
+         * @param {JQLite} element
+         */
+        link(scope, element) {
+            $timeout(() => element[0].focus(), 100);
+        }
+    };
+}
+
+directive.$inject = ['$timeout'];
diff --git a/modules/frontend/app/directives/btn-ignite-link.js b/modules/frontend/app/directives/btn-ignite-link.js
new file mode 100644
index 0000000..02aaf8f
--- /dev/null
+++ b/modules/frontend/app/directives/btn-ignite-link.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default function() {
+    return {
+        restrict: 'C',
+        link: (scope, $element) => {
+            $element.contents()
+                .filter(function() {
+                    return this.nodeType === 3;
+                })
+                .wrap('<span></span>');
+        }
+    };
+}
diff --git a/modules/frontend/app/directives/copy-to-clipboard.directive.js b/modules/frontend/app/directives/copy-to-clipboard.directive.js
new file mode 100644
index 0000000..399e1f3
--- /dev/null
+++ b/modules/frontend/app/directives/copy-to-clipboard.directive.js
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @param {ReturnType<typeof import('../services/CopyToClipboard.service').default>} CopyToClipboard
+ */
+export default function directive(CopyToClipboard) {
+    return {
+        restrict: 'A',
+        /**
+         * @param {ng.IScope} scope
+         * @param {JQLite} element
+         * @param {ng.IAttributes} attrs
+         */
+        link(scope, element, attrs) {
+            element.bind('click', () => CopyToClipboard.copy(attrs.igniteCopyToClipboard));
+
+            if (!document.queryCommandSupported('copy'))
+                element.hide();
+        }
+    };
+}
+
+directive.$inject = ['IgniteCopyToClipboard'];
diff --git a/modules/frontend/app/directives/hide-on-state-change/hide-on-state-change.directive.js b/modules/frontend/app/directives/hide-on-state-change/hide-on-state-change.directive.js
new file mode 100644
index 0000000..cd1f45a
--- /dev/null
+++ b/modules/frontend/app/directives/hide-on-state-change/hide-on-state-change.directive.js
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @param {import('@uirouter/angularjs').TransitionService} $transitions
+ */
+export default function directive($transitions) {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} element
+     */
+    const link = (scope, element) => {
+        $transitions.onSuccess({}, () => {element.fadeOut('slow');});
+    };
+
+    return {
+        restrict: 'AE',
+        link
+    };
+}
+
+directive.$inject = ['$transitions'];
diff --git a/modules/frontend/app/directives/match.directive.js b/modules/frontend/app/directives/match.directive.js
new file mode 100644
index 0000000..940ca08
--- /dev/null
+++ b/modules/frontend/app/directives/match.directive.js
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Directive to enable validation to match specified value.
+export default function() {
+    return {
+        require: {
+            ngModel: 'ngModel'
+        },
+        scope: false,
+        bindToController: {
+            igniteMatch: '<'
+        },
+        controller: class {
+            /** @type {ng.INgModelController} */
+            ngModel;
+            /** @type {string} */
+            igniteMatch;
+
+            $postLink() {
+                this.ngModel.$overrideModelOptions({allowInvalid: true});
+                this.ngModel.$validators.mismatch = (value) => value === this.igniteMatch;
+            }
+
+            /**
+             * @param {{igniteMatch: ng.IChangesObject<string>}} changes
+             */
+            $onChanges(changes) {
+                if ('igniteMatch' in changes) this.ngModel.$validate();
+            }
+        }
+    };
+}
diff --git a/modules/frontend/app/directives/match.directive.spec.js b/modules/frontend/app/directives/match.directive.spec.js
new file mode 100644
index 0000000..c274585
--- /dev/null
+++ b/modules/frontend/app/directives/match.directive.spec.js
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'mocha';
+import {assert} from 'chai';
+import angular from 'angular';
+import directive from './match.directive';
+
+/**
+ * @param {HTMLInputElement} el
+ * @returns {ng.INgModelController}
+ */
+const ngModel = (el) => angular.element(el).data().$ngModelController;
+
+suite('ignite-match', () => {
+    /** @type {ng.IScope} */
+    let $scope;
+    /** @type {ng.ICompileService} */
+    let $compile;
+
+    setup(() => {
+        angular.module('test', []).directive('igniteMatch', directive);
+        angular.mock.module('test');
+        angular.mock.inject((_$rootScope_, _$compile_) => {
+            $compile = _$compile_;
+            $scope = _$rootScope_.$new();
+        });
+    });
+
+    test('Matching', () => {
+        const el = angular.element(`
+            <input type="password" ng-model="data.master"/>
+            <input type="password" ng-model="data.slave" ignite-match="data.master"/>
+        `);
+
+        const setValue = (el, value) => {
+            ngModel(el).$setViewValue(value, 'input');
+            $scope.$digest();
+        };
+
+        $scope.data = {};
+        $compile(el)($scope);
+        $scope.$digest();
+
+        // const [master, , slave] = el;
+        // For some reason, this code not work after Babel, replaced with 'old' syntax.
+        const master = el[0];
+        const slave = el[2];
+
+        setValue(slave, '123');
+        $scope.$digest();
+
+        assert.isTrue(
+            slave.classList.contains('ng-invalid-mismatch'),
+            `Invalidates if slave input changes value and it doesn't match master value`
+        );
+        assert.equal(
+            $scope.data.slave,
+            '123',
+            'Allows invalid value into model'
+        );
+
+        setValue(master, '123');
+
+        assert.isFalse(
+            slave.classList.contains('ng-invalid-mismatch'),
+            `Runs validation on master value change`
+        );
+    });
+});
diff --git a/modules/frontend/app/directives/on-click-focus.directive.js b/modules/frontend/app/directives/on-click-focus.directive.js
new file mode 100644
index 0000000..bbc46d1
--- /dev/null
+++ b/modules/frontend/app/directives/on-click-focus.directive.js
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Directive to describe element that should be focused on click.
+ * @param {ReturnType<typeof import('../services/Focus.service').default>} Focus
+ */
+export default function directive(Focus) {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} elem
+     * @param {ng.IAttributes} attrs
+     */
+    function directive(scope, elem, attrs) {
+        elem.on('click', () => Focus.move(attrs.igniteOnClickFocus));
+
+        // Removes bound events in the element itself when the scope is destroyed
+        scope.$on('$destroy', () => elem.off('click'));
+    }
+
+    return directive;
+}
+
+directive.$inject = ['IgniteFocus'];
diff --git a/modules/frontend/app/directives/on-enter-focus-move.directive.js b/modules/frontend/app/directives/on-enter-focus-move.directive.js
new file mode 100644
index 0000000..9cb9913
--- /dev/null
+++ b/modules/frontend/app/directives/on-enter-focus-move.directive.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Directive to move focus to specified element on ENTER key.
+ * @param {ReturnType<typeof import('../services/Focus.service').default>} Focus
+ */
+export default function directive(Focus) {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} elem
+     * @param {ng.IAttributes} attrs
+     */
+    function directive(scope, elem, attrs) {
+        elem.on('keydown keypress', (event) => {
+            if (event.which === 13) {
+                event.preventDefault();
+
+                Focus.move(attrs.igniteOnEnterFocusMove);
+            }
+        });
+    }
+
+    return directive;
+}
+
+directive.$inject = ['IgniteFocus'];
diff --git a/modules/frontend/app/directives/on-enter.directive.js b/modules/frontend/app/directives/on-enter.directive.js
new file mode 100644
index 0000000..1bc3e48
--- /dev/null
+++ b/modules/frontend/app/directives/on-enter.directive.js
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Directive to bind ENTER key press with some user action.
+ * @param {ng.ITimeoutService} $timeout
+ */
+export default function directive($timeout) {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} elem
+     * @param {ng.IAttributes} attrs
+     */
+    function directive(scope, elem, attrs) {
+        elem.on('keydown keypress', (event) => {
+            if (event.which === 13) {
+                scope.$apply(() => $timeout(() => scope.$eval(attrs.igniteOnEnter)));
+
+                event.preventDefault();
+            }
+        });
+
+        // Removes bound events in the element itself when the scope is destroyed.
+        scope.$on('$destroy', () => elem.off('keydown keypress'));
+    }
+
+    return directive;
+}
+
+directive.$inject = ['$timeout'];
diff --git a/modules/frontend/app/directives/on-escape.directive.js b/modules/frontend/app/directives/on-escape.directive.js
new file mode 100644
index 0000000..b1f1648
--- /dev/null
+++ b/modules/frontend/app/directives/on-escape.directive.js
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Directive to bind ESC key press with some user action.
+ * @param {ng.ITimeoutService} $timeout
+ */
+export default function directive($timeout) {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} elem
+     * @param {ng.IAttributes} attrs
+     */
+    function directive(scope, elem, attrs) {
+        elem.on('keydown keypress', (event) => {
+            if (event.which === 27) {
+                scope.$apply(() => $timeout(() => scope.$eval(attrs.igniteOnEscape)));
+
+                event.preventDefault();
+            }
+        });
+
+        // Removes bound events in the element itself when the scope is destroyed.
+        scope.$on('$destroy', () => elem.off('keydown keypress'));
+    }
+
+    return directive;
+}
+
+directive.$inject = ['$timeout'];
diff --git a/modules/frontend/app/directives/on-focus-out.directive.js b/modules/frontend/app/directives/on-focus-out.directive.js
new file mode 100644
index 0000000..fe59477
--- /dev/null
+++ b/modules/frontend/app/directives/on-focus-out.directive.js
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @type {ng.IComponentController}
+ */
+class OnFocusOutController {
+    /** @type {OnFocusOutController} */
+    parent;
+    /** @type {Array<OnFocusOutController>} */
+    children = [];
+    /** @type {Array<string>} */
+    ignoredClasses = [];
+    /** @type {function} */
+    igniteOnFocusOut;
+
+    static $inject = ['$element', '$window', '$scope'];
+    /**
+     * @param {JQLite} $element
+     * @param {ng.IWindowService} $window 
+     * @param {ng.IScope} $scope
+     */
+    constructor($element, $window, $scope) {
+        this.$element = $element;
+        this.$window = $window;
+        this.$scope = $scope;
+
+        /** @param {MouseEvent|FocusEvent} e */
+        this._eventHandler = (e) => {
+            this.children.forEach((c) => c._eventHandler(e));
+            if (this.shouldPropagate(e) && this.isFocused) {
+                this.$scope.$applyAsync(() => {
+                    this.igniteOnFocusOut();
+                    this.isFocused = false;
+                });
+            }
+        };
+        /** @param {FocusEvent} e */
+        this._onFocus = (e) => {
+            this.isFocused = true;
+        };
+    }
+    $onDestroy() {
+        this.$window.removeEventListener('click', this._eventHandler, true);
+        this.$window.removeEventListener('focusin', this._eventHandler, true);
+        this.$element[0].removeEventListener('focus', this._onFocus, true);
+        if (this.parent) this.parent.children.splice(this.parent.children.indexOf(this), 1);
+        this.$element = this.$window = this._eventHandler = this._onFocus = null;
+    }
+    shouldPropagate(e) {
+        return !this.targetHasIgnoredClasses(e) && this.targetIsOutOfElement(e);
+    }
+    targetIsOutOfElement(e) {
+        return !this.$element.find(e.target).length;
+    }
+    targetHasIgnoredClasses(e) {
+        return this.ignoredClasses.some((c) => e.target.classList.contains(c));
+    }
+    /**
+     * @param {ng.IOnChangesObject} changes [description]
+     */
+    $onChanges(changes) {
+        if (
+            'ignoredClasses' in changes &&
+            changes.ignoredClasses.currentValue !== changes.ignoredClasses.previousValue
+        )
+            this.ignoredClasses = changes.ignoredClasses.currentValue.split(' ').concat('body-overlap');
+    }
+    $onInit() {
+        if (this.parent) this.parent.children.push(this);
+    }
+    $postLink() {
+        this.$window.addEventListener('click', this._eventHandler, true);
+        this.$window.addEventListener('focusin', this._eventHandler, true);
+        this.$element[0].addEventListener('focus', this._onFocus, true);
+    }
+}
+
+/**
+ * @type {ng.IDirectiveFactory}
+ */
+export default function() {
+    return {
+        controller: OnFocusOutController,
+        require: {
+            parent: '^^?igniteOnFocusOut'
+        },
+        bindToController: {
+            igniteOnFocusOut: '&',
+            ignoredClasses: '@?igniteOnFocusOutIgnoredClasses'
+        }
+    };
+}
diff --git a/modules/frontend/app/directives/retain-selection.directive.js b/modules/frontend/app/directives/retain-selection.directive.js
new file mode 100644
index 0000000..fb7321a
--- /dev/null
+++ b/modules/frontend/app/directives/retain-selection.directive.js
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Directive to workaround known issue with type ahead edit lost cursor position.
+ * @param {ng.ITimeoutService} $timeout
+ */
+export default function directive($timeout) {
+    let promise;
+
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} elem  [description]
+     */
+    function directive(scope, elem) {
+        elem.on('keydown', function(evt) {
+            const key = evt.which;
+            const ctrlDown = evt.ctrlKey || evt.metaKey;
+            const input = this;
+            let start = input.selectionStart;
+
+            if (promise)
+                $timeout.cancel(promise);
+
+            promise = $timeout(() => {
+                let setCursor = false;
+
+                // Handle Backspace[8].
+                if (key === 8 && start > 0) {
+                    start -= 1;
+
+                    setCursor = true;
+                }
+                // Handle Del[46].
+                else if (key === 46)
+                    setCursor = true;
+                // Handle: Caps Lock[20], Tab[9], Shift[16], Ctrl[17], Alt[18], Esc[27], Enter[13], Arrows[37..40], Home[36], End[35], Ins[45], PgUp[33], PgDown[34], F1..F12[111..124], Num Lock[], Scroll Lock[145].
+                else if (!(key === 8 || key === 9 || key === 13 || (key > 15 && key < 20) || key === 27 ||
+                    (key > 32 && key < 41) || key === 45 || (key > 111 && key < 124) || key === 144 || key === 145)) {
+                    // Handle: Ctrl + [A[65], C[67], V[86]].
+                    if (!(ctrlDown && (key === 65 || key === 67 || key === 86))) {
+                        start += 1;
+
+                        setCursor = true;
+                    }
+                }
+
+                if (setCursor)
+                    input.setSelectionRange(start, start);
+
+                promise = null;
+            });
+        });
+
+        // Removes bound events in the element itself when the scope is destroyed
+        scope.$on('$destroy', function() {
+            elem.off('keydown');
+        });
+    }
+
+    return directive;
+}
+
+directive.$inject = ['$timeout'];
diff --git a/modules/frontend/app/errors/CancellationError.js b/modules/frontend/app/errors/CancellationError.js
new file mode 100644
index 0000000..17f0035
--- /dev/null
+++ b/modules/frontend/app/errors/CancellationError.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class CancellationError extends Error {
+    constructor(message = 'Cancelled by user') {
+        super(message);
+
+        // Workaround for Babel issue with extend: https://github.com/babel/babel/issues/3083#issuecomment-315569824
+        this.constructor = CancellationError;
+
+        // eslint-disable-next-line no-proto
+        this.__proto__ = CancellationError.prototype;
+    }
+}
diff --git a/modules/frontend/app/filters/byName.filter.js b/modules/frontend/app/filters/byName.filter.js
new file mode 100644
index 0000000..6ec56f4
--- /dev/null
+++ b/modules/frontend/app/filters/byName.filter.js
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+export default () => (arr, search) => {
+    if (!(arr && arr.length) || !search)
+        return arr;
+
+    return _.filter(arr, ({ name }) => name.indexOf(search) >= 0);
+};
diff --git a/modules/frontend/app/filters/bytes.filter.js b/modules/frontend/app/filters/bytes.filter.js
new file mode 100644
index 0000000..71e77d4
--- /dev/null
+++ b/modules/frontend/app/filters/bytes.filter.js
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default () => {
+    /**
+     * @param {number} bytes
+     * @param {number} [precision]
+     */
+    const filter = (bytes, precision) => {
+        if (bytes === 0)
+            return '0 bytes';
+
+        if (isNaN(parseFloat(bytes)) || !isFinite(bytes))
+            return '-';
+
+        if (typeof precision === 'undefined')
+            precision = 1;
+
+        const units = ['bytes', 'kB', 'MB', 'GB', 'TB'];
+        const number = Math.floor(Math.log(bytes) / Math.log(1024));
+
+        return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number];
+    };
+
+    return filter;
+};
diff --git a/modules/frontend/app/filters/bytes.filter.spec.js b/modules/frontend/app/filters/bytes.filter.spec.js
new file mode 100644
index 0000000..fd92db3
--- /dev/null
+++ b/modules/frontend/app/filters/bytes.filter.spec.js
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import bytesFilter from './bytes.filter';
+
+import {suite, test} from 'mocha';
+import {assert} from 'chai';
+
+const bytesFilterInstance = bytesFilter();
+
+suite('bytes filter', () => {
+    test('bytes filter', () => {
+        assert.equal(bytesFilterInstance(0), '0 bytes');
+        assert.equal(bytesFilterInstance(1000), '1000.0 bytes');
+        assert.equal(bytesFilterInstance(1024), '1.0 kB');
+        assert.equal(bytesFilterInstance(5000), '4.9 kB');
+        assert.equal(bytesFilterInstance(1048576), '1.0 MB');
+        assert.equal(bytesFilterInstance(104857600), '100.0 MB');
+        assert.equal(bytesFilterInstance(1073741824), '1.0 GB');
+        assert.equal(bytesFilterInstance(1099511627776), '1.0 TB');
+    });
+});
diff --git a/modules/frontend/app/filters/default-name.filter.js b/modules/frontend/app/filters/default-name.filter.js
new file mode 100644
index 0000000..a8b182e
--- /dev/null
+++ b/modules/frontend/app/filters/default-name.filter.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+export default () => {
+    /**
+     * Filter that will check name and return `<default>` if needed.
+     * @param {string} name
+     * @param {string} html
+     */
+    const filter = (name, html) => _.isEmpty(name) ? (html ? '&lt;default&gt;' : '<default>') : name;
+
+    return filter;
+};
diff --git a/modules/frontend/app/filters/domainsValidation.filter.js b/modules/frontend/app/filters/domainsValidation.filter.js
new file mode 100644
index 0000000..d551ca5
--- /dev/null
+++ b/modules/frontend/app/filters/domainsValidation.filter.js
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+/**
+ * @param {ReturnType<typeof import('../services/LegacyUtils.service').default>} LegacyUtils [description]
+ */
+export default function factory(LegacyUtils) {
+    /**
+     * Filter domain models with key fields configuration.
+     * @template T
+     * @param {Array<T>} domains
+     * @param {boolean} valid
+     * @param {boolean} invalid
+     */
+    const filter = (domains, valid, invalid) => {
+        if (valid && invalid)
+            return domains;
+
+        /** @type {Array<T>} */
+        const out = [];
+
+        _.forEach(domains, function(domain) {
+            const _valid = !LegacyUtils.domainForStoreConfigured(domain) || LegacyUtils.isJavaBuiltInClass(domain.keyType) || !_.isEmpty(domain.keyFields);
+
+            if (valid && _valid || invalid && !_valid)
+                out.push(domain);
+        });
+
+        return out;
+    };
+
+    return filter;
+}
+
+factory.$inject = ['IgniteLegacyUtils'];
diff --git a/modules/frontend/app/filters/duration.filter.js b/modules/frontend/app/filters/duration.filter.js
new file mode 100644
index 0000000..0ce4226
--- /dev/null
+++ b/modules/frontend/app/filters/duration.filter.js
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default () => {
+    /**
+     * @param {number} t Time in ms.
+     * @param {string} dflt Default value.
+     */
+    const filter = (t, dflt = '0') => {
+        if (t === 9223372036854775807)
+            return 'Infinite';
+
+        if (t <= 0)
+            return dflt;
+
+        const a = (i, suffix) => i && i !== '00' ? i + suffix + ' ' : '';
+
+        const cd = 24 * 60 * 60 * 1000;
+        const ch = 60 * 60 * 1000;
+        const cm = 60 * 1000;
+        const cs = 1000;
+
+        const d = Math.floor(t / cd);
+        const h = Math.floor((t - d * cd) / ch);
+        const m = Math.floor((t - d * cd - h * ch) / cm);
+        const s = Math.floor((t - d * cd - h * ch - m * cm) / cs);
+        const ms = Math.round(t % 1000);
+
+        return a(d, 'd') + a(h, 'h') + a(m, 'm') + a(s, 's') + (t < 1000 || (t < cm && ms !== 0) ? ms + 'ms' : '');
+    };
+
+    return filter;
+};
diff --git a/modules/frontend/app/filters/hasPojo.filter.js b/modules/frontend/app/filters/hasPojo.filter.js
new file mode 100644
index 0000000..4186928
--- /dev/null
+++ b/modules/frontend/app/filters/hasPojo.filter.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Filter that return 'true' if caches has at least one domain with 'generatePojo' flag.
+export default () => ({caches} = []) =>
+    _.find(caches, (cache) => cache.domains && cache.domains.length &&
+        cache.domains.find((domain) => domain.generatePojo));
diff --git a/modules/frontend/app/filters/id8.filter.js b/modules/frontend/app/filters/id8.filter.js
new file mode 100644
index 0000000..6a173c8
--- /dev/null
+++ b/modules/frontend/app/filters/id8.filter.js
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import id8 from 'app/utils/id8';
+
+export default function() {
+    return id8;
+}
diff --git a/modules/frontend/app/filters/uiGridSubcategories.filter.js b/modules/frontend/app/filters/uiGridSubcategories.filter.js
new file mode 100644
index 0000000..859192d
--- /dev/null
+++ b/modules/frontend/app/filters/uiGridSubcategories.filter.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+export default () => {
+    return (arr, category) => {
+        return _.filter(arr, (item) => {
+            return item.colDef.categoryDisplayName === category;
+        });
+    };
+};
diff --git a/modules/frontend/app/helpers/jade/mixins.pug b/modules/frontend/app/helpers/jade/mixins.pug
new file mode 100644
index 0000000..6b9c287
--- /dev/null
+++ b/modules/frontend/app/helpers/jade/mixins.pug
@@ -0,0 +1,22 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include ../../primitives/btn-group/index
+include ../../primitives/datepicker/index
+include ../../primitives/timepicker/index
+include ../../primitives/dropdown/index
+include ../../primitives/switcher/index
+include ../../primitives/form-field/index
diff --git a/modules/frontend/app/modules/ace.module.js b/modules/frontend/app/modules/ace.module.js
new file mode 100644
index 0000000..4decbe1
--- /dev/null
+++ b/modules/frontend/app/modules/ace.module.js
@@ -0,0 +1,284 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import _ from 'lodash';
+
+angular
+    .module('ignite-console.ace', [])
+    .constant('igniteAceConfig', {})
+    .directive('igniteAce', ['igniteAceConfig', function(aceConfig) {
+        if (_.isUndefined(window.ace))
+            throw new Error('ignite-ace need ace to work... (o rly?)');
+
+        /**
+         * Sets editor options such as the wrapping mode or the syntax checker.
+         *
+         * The supported options are:
+         *
+         *   <ul>
+         *     <li>showGutter</li>
+         *     <li>useWrapMode</li>
+         *     <li>onLoad</li>
+         *     <li>theme</li>
+         *     <li>mode</li>
+         *   </ul>
+         *
+         * @param acee
+         * @param session ACE editor session.
+         * @param {object} opts Options to be set.
+         */
+        const setOptions = (acee, session, opts) => {
+            // Sets the ace worker path, if running from concatenated or minified source.
+            if (!_.isUndefined(opts.workerPath)) {
+                const config = window.ace.acequire('ace/config');
+
+                config.set('workerPath', opts.workerPath);
+            }
+
+            // Ace requires loading.
+            _.forEach(opts.require, (n) => window.ace.acequire(n));
+
+            // Boolean options.
+            if (!_.isUndefined(opts.showGutter))
+                acee.renderer.setShowGutter(opts.showGutter);
+
+            if (!_.isUndefined(opts.useWrapMode))
+                session.setUseWrapMode(opts.useWrapMode);
+
+            if (!_.isUndefined(opts.showInvisibles))
+                acee.renderer.setShowInvisibles(opts.showInvisibles);
+
+            if (!_.isUndefined(opts.showIndentGuides))
+                acee.renderer.setDisplayIndentGuides(opts.showIndentGuides);
+
+            if (!_.isUndefined(opts.useSoftTabs))
+                session.setUseSoftTabs(opts.useSoftTabs);
+
+            if (!_.isUndefined(opts.showPrintMargin))
+                acee.setShowPrintMargin(opts.showPrintMargin);
+
+            // Commands.
+            if (!_.isUndefined(opts.disableSearch) && opts.disableSearch) {
+                acee.commands.addCommands([{
+                    name: 'unfind',
+                    bindKey: {
+                        win: 'Ctrl-F',
+                        mac: 'Command-F'
+                    },
+                    exec: _.constant(false),
+                    readOnly: true
+                }]);
+            }
+
+            // Base options.
+            if (_.isString(opts.theme))
+                acee.setTheme('ace/theme/' + opts.theme);
+
+            if (_.isString(opts.mode))
+                session.setMode('ace/mode/' + opts.mode);
+
+            if (!_.isUndefined(opts.firstLineNumber)) {
+                if (_.isNumber(opts.firstLineNumber))
+                    session.setOption('firstLineNumber', opts.firstLineNumber);
+                else if (_.isFunction(opts.firstLineNumber))
+                    session.setOption('firstLineNumber', opts.firstLineNumber());
+            }
+
+            // Advanced options.
+            if (!_.isUndefined(opts.advanced)) {
+                for (const key in opts.advanced) {
+                    if (opts.advanced.hasOwnProperty(key)) {
+                        // Create a javascript object with the key and value.
+                        const obj = {name: key, value: opts.advanced[key]};
+
+                        // Try to assign the option to the ace editor.
+                        acee.setOption(obj.name, obj.value);
+                    }
+                }
+            }
+
+            // Advanced options for the renderer.
+            if (!_.isUndefined(opts.rendererOptions)) {
+                for (const key in opts.rendererOptions) {
+                    if (opts.rendererOptions.hasOwnProperty(key)) {
+                        // Create a javascript object with the key and value.
+                        const obj = {name: key, value: opts.rendererOptions[key]};
+
+                        // Try to assign the option to the ace editor.
+                        acee.renderer.setOption(obj.name, obj.value);
+                    }
+                }
+            }
+
+            // onLoad callbacks.
+            _.forEach(opts.callbacks, (cb) => {
+                if (_.isFunction(cb))
+                    cb(acee);
+            });
+        };
+
+        return {
+            restrict: 'EA',
+            require: ['?ngModel', '?^form', 'igniteAce'],
+            bindToController: {
+                onSelectionChange: '&?'
+            },
+            controller() {},
+            link: (scope, elm, attrs, [ngModel, form, igniteAce]) => {
+                /**
+                 * Corresponds the igniteAceConfig ACE configuration.
+                 *
+                 * @type object
+                 */
+                const options = aceConfig.ace || {};
+
+                /**
+                 * IgniteAceConfig merged with user options via json in attribute or data binding.
+                 *
+                 * @type object
+                 */
+                let opts = Object.assign({}, options, scope.$eval(attrs.igniteAce));
+
+                /**
+                 * ACE editor.
+                 *
+                 * @type object
+                 */
+                const acee = window.ace.edit(elm[0]);
+
+                /**
+                 * ACE editor session.
+                 *
+                 * @type object
+                 * @see [EditSession]{@link http://ace.c9.io/#nav=api&api=edit_session}
+                 */
+                const session = acee.getSession();
+
+                const selection = session.getSelection();
+
+                /**
+                 * Reference to a change listener created by the listener factory.
+                 *
+                 * @function
+                 * @see listenerFactory.onChange
+                 */
+                let onChangeListener;
+
+                /**
+                 * Creates a change listener which propagates the change event and the editor session
+                 * to the callback from the user option onChange.
+                 * It might be exchanged during runtime, if this happens the old listener will be unbound.
+                 *
+                 * @param callback Callback function defined in the user options.
+                 * @see onChangeListener
+                 */
+                const onChangeFactory = (callback) => {
+                    return (e) => {
+                        const newValue = session.getValue();
+
+                        // HACK make sure to only trigger the apply outside of the
+                        // digest loop 'cause ACE is actually using this callback
+                        // for any text transformation !
+                        if (ngModel && newValue !== ngModel.$viewValue &&
+                            !scope.$$phase && !scope.$root.$$phase)
+                            scope.$eval(() => ngModel.$setViewValue(newValue));
+
+                        if (!_.isUndefined(callback)) {
+                            scope.$evalAsync(() => {
+                                if (_.isFunction(callback))
+                                    callback([e, acee]);
+                                else
+                                    throw new Error('ignite-ace use a function as callback');
+                            });
+                        }
+                    };
+                };
+
+                attrs.$observe('readonly', (value) => acee.setReadOnly(!!value || value === ''));
+
+                // Value Blind.
+                if (ngModel) {
+                    // Remove "ngModel" controller from parent form for correct dirty checks.
+                    form && form.$removeControl(ngModel);
+
+                    ngModel.$formatters.push((value) => {
+                        if (_.isUndefined(value) || value === null)
+                            return '';
+
+                        if (_.isObject(value) || _.isArray(value))
+                            throw new Error('ignite-ace cannot use an object or an array as a model');
+
+                        return value;
+                    });
+
+                    ngModel.$render = () => session.setValue(ngModel.$viewValue);
+
+                    acee.on('change', () => ngModel.$setViewValue(acee.getValue()));
+
+                    selection.on('changeSelection', () => {
+                        if (igniteAce.onSelectionChange) {
+                            const aceSelection = selection.isEmpty() ? null : acee.session.getTextRange(acee.getSelectionRange());
+
+                            igniteAce.onSelectionChange({$event: aceSelection});
+                        }
+                    });
+                }
+
+                // Listen for option updates.
+                const updateOptions = (current, previous) => {
+                    if (current === previous)
+                        return;
+
+                    opts = Object.assign({}, options, scope.$eval(attrs.igniteAce));
+
+                    opts.callbacks = [opts.onLoad];
+
+                    // Also call the global onLoad handler.
+                    if (opts.onLoad !== options.onLoad)
+                        opts.callbacks.unshift(options.onLoad);
+
+                    // Unbind old change listener.
+                    session.removeListener('change', onChangeListener);
+
+                    // Bind new change listener.
+                    onChangeListener = onChangeFactory(opts.onChange);
+
+                    session.on('change', onChangeListener);
+
+                    setOptions(acee, session, opts);
+                };
+
+                scope.$watch(attrs.igniteAce, updateOptions, /* deep watch */ true);
+
+                // Set the options here, even if we try to watch later,
+                // if this line is missing things go wrong (and the tests will also fail).
+                updateOptions(options);
+
+                elm.on('$destroy', () => {
+                    acee.session.$stopWorker();
+                    acee.destroy();
+                });
+
+                scope.$watch(() => [elm[0].offsetWidth, elm[0].offsetHeight],
+                    () => {
+                        acee.resize();
+                        acee.renderer.updateFull();
+                    }, true);
+            }
+        };
+    }]);
diff --git a/modules/frontend/app/modules/agent/AgentManager.service.js b/modules/frontend/app/modules/agent/AgentManager.service.js
new file mode 100644
index 0000000..878d17d
--- /dev/null
+++ b/modules/frontend/app/modules/agent/AgentManager.service.js
@@ -0,0 +1,861 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+import {nonEmpty, nonNil} from 'app/utils/lodashMixins';
+
+import {BehaviorSubject} from 'rxjs';
+import {distinctUntilChanged, filter, first, map, pluck, tap} from 'rxjs/operators';
+
+import io from 'socket.io-client';
+
+import AgentModal from './AgentModal.service';
+// @ts-ignore
+import Worker from './decompress.worker';
+import SimpleWorkerPool from '../../utils/SimpleWorkerPool';
+import maskNull from 'app/core/utils/maskNull';
+
+import {CancellationError} from 'app/errors/CancellationError';
+import {ClusterSecretsManager} from './types/ClusterSecretsManager';
+import ClusterLoginService from './components/cluster-login/service';
+
+const State = {
+    INIT: 'INIT',
+    AGENT_DISCONNECTED: 'AGENT_DISCONNECTED',
+    CLUSTER_DISCONNECTED: 'CLUSTER_DISCONNECTED',
+    CONNECTED: 'CONNECTED'
+};
+
+const IGNITE_2_0 = '2.0.0';
+const LAZY_QUERY_SINCE = [['2.1.4-p1', '2.2.0'], '2.2.1'];
+const COLLOCATED_QUERY_SINCE = [['2.3.5', '2.4.0'], ['2.4.6', '2.5.0'], ['2.5.1-p13', '2.6.0'], '2.7.0'];
+const COLLECT_BY_CACHE_GROUPS_SINCE = '2.7.0';
+const QUERY_PING_SINCE = [['2.5.6', '2.6.0'], '2.7.4'];
+
+/**
+ * Query execution result.
+ * @typedef {{responseNodeId: String, queryId: String, columns: String[], rows: {Object[][]}, hasMore: Boolean, duration: Number}} VisorQueryResult
+ */
+
+/**
+ * Query ping result.
+ * @typedef {{}} VisorQueryPingResult
+ */
+
+/** Reserved cache names */
+const RESERVED_CACHE_NAMES = [
+    'ignite-hadoop-mr-sys-cache',
+    'ignite-sys-cache',
+    'MetaStorage',
+    'TxLog'
+];
+
+/** Error codes from o.a.i.internal.processors.restGridRestResponse.java */
+const SuccessStatus = {
+    /** Command succeeded. */
+    STATUS_SUCCESS: 0,
+    /** Command failed. */
+    STATUS_FAILED: 1,
+    /** Authentication failure. */
+    AUTH_FAILED: 2,
+    /** Security check failed. */
+    SECURITY_CHECK_FAILED: 3
+};
+
+class ConnectionState {
+    constructor(cluster) {
+        this.cluster = cluster;
+        this.clusters = [];
+        this.state = State.INIT;
+    }
+
+    updateCluster(cluster) {
+        this.cluster = cluster;
+        this.cluster.connected = !!_.find(this.clusters, {id: this.cluster.id});
+
+        return cluster;
+    }
+
+    update(demo, count, clusters, hasDemo) {
+        this.clusters = clusters;
+
+        if (_.isEmpty(this.clusters))
+            this.cluster = null;
+
+        if (_.isNil(this.cluster))
+            this.cluster = _.head(clusters);
+
+        if (this.cluster)
+            this.cluster.connected = !!_.find(clusters, {id: this.cluster.id});
+
+        this.hasDemo = hasDemo;
+
+        if (count === 0)
+            this.state = State.AGENT_DISCONNECTED;
+        else if (demo || _.get(this.cluster, 'connected'))
+            this.state = State.CONNECTED;
+        else
+            this.state = State.CLUSTER_DISCONNECTED;
+    }
+
+    useConnectedCluster() {
+        if (nonEmpty(this.clusters) && !this.cluster.connected) {
+            this.cluster = _.head(this.clusters);
+
+            this.cluster.connected = true;
+
+            this.state = State.CONNECTED;
+        }
+    }
+
+    disconnect() {
+        if (this.cluster)
+            this.cluster.disconnect = true;
+
+        this.clusters = [];
+        this.state = State.AGENT_DISCONNECTED;
+    }
+}
+
+export default class AgentManager {
+    static $inject = ['$rootScope', '$q', '$transitions', 'AgentModal', 'UserNotifications', 'IgniteVersion', 'ClusterLoginService'];
+
+    /** @type {ng.IScope} */
+    $root;
+
+    /** @type {ng.IQService} */
+    $q;
+
+    /** @type {AgentModal} */
+    agentModal;
+
+    /** @type {ClusterLoginService} */
+    ClusterLoginSrv;
+
+    /** @type {String} */
+    clusterVersion;
+
+    connectionSbj = new BehaviorSubject(new ConnectionState(AgentManager.restoreActiveCluster()));
+
+    /** @type {ClusterSecretsManager} */
+    clustersSecrets = new ClusterSecretsManager();
+
+    pool = new SimpleWorkerPool('decompressor', Worker, 4);
+
+    /** @type {Set<ng.IPromise<unknown>>} */
+    promises = new Set();
+
+    socket = null;
+
+    /** @type {Set<() => Promise>} */
+    switchClusterListeners = new Set();
+
+    addClusterSwitchListener(func) {
+        this.switchClusterListeners.add(func);
+    }
+
+    removeClusterSwitchListener(func) {
+        this.switchClusterListeners.delete(func);
+    }
+
+    static restoreActiveCluster() {
+        try {
+            return JSON.parse(localStorage.cluster);
+        }
+        catch (ignore) {
+            return null;
+        }
+        finally {
+            localStorage.removeItem('cluster');
+        }
+    }
+
+    /**
+     * @param {ng.IRootScopeService} $root
+     * @param {ng.IQService} $q
+     * @param {import('@uirouter/angularjs').TransitionService} $transitions
+     * @param {import('./AgentModal.service').default} agentModal
+     * @param {import('app/components/user-notifications/service').default} UserNotifications
+     * @param {import('app/services/Version.service').default} Version
+     * @param {import('./components/cluster-login/service').default} ClusterLoginSrv
+     */
+    constructor($root, $q, $transitions, agentModal, UserNotifications, Version, ClusterLoginSrv) {
+        this.$root = $root;
+        this.$q = $q;
+        this.$transitions = $transitions;
+        this.agentModal = agentModal;
+        this.UserNotifications = UserNotifications;
+        this.Version = Version;
+        this.ClusterLoginSrv = ClusterLoginSrv;
+
+        this.clusterVersion = this.Version.webConsole;
+
+        let prevCluster;
+
+        this.currentCluster$ = this.connectionSbj.pipe(
+            distinctUntilChanged(({ cluster }) => prevCluster === cluster),
+            tap(({ cluster }) => prevCluster = cluster)
+        );
+
+        this.clusterIsActive$ = this.connectionSbj.pipe(
+            map(({ cluster }) => cluster),
+            filter((cluster) => Boolean(cluster)),
+            pluck('active')
+        );
+
+        this.clusterIsAvailable$ = this.connectionSbj.pipe(
+            pluck('cluster'),
+            map((cluster) => !!cluster)
+        );
+
+        if (!this.isDemoMode()) {
+            this.connectionSbj.subscribe({
+                next: ({cluster}) => {
+                    const version = this.getClusterVersion(cluster);
+
+                    if (_.isEmpty(version))
+                        return;
+
+                    this.clusterVersion = version;
+                }
+            });
+        }
+    }
+
+    isDemoMode() {
+        return this.$root.IgniteDemoMode;
+    }
+
+    getClusterVersion(cluster) {
+        return _.get(cluster, 'clusterVersion');
+    }
+
+    available(...sinceVersion) {
+        return this.Version.since(this.clusterVersion, ...sinceVersion);
+    }
+
+    connect() {
+        if (nonNil(this.socket))
+            return;
+
+        const options = this.isDemoMode() ? {query: 'IgniteDemoMode=true'} : {};
+
+        this.socket = io.connect(options);
+
+        const onDisconnect = () => {
+            const conn = this.connectionSbj.getValue();
+
+            conn.disconnect();
+
+            this.connectionSbj.next(conn);
+        };
+
+        this.socket.on('connect_error', onDisconnect);
+
+        this.socket.on('disconnect', onDisconnect);
+
+        this.socket.on('agents:stat', ({clusters, count, hasDemo}) => {
+            const conn = this.connectionSbj.getValue();
+
+            conn.update(this.isDemoMode(), count, clusters, hasDemo);
+
+            this.connectionSbj.next(conn);
+        });
+
+        this.socket.on('cluster:changed', (cluster) => this.updateCluster(cluster));
+
+        this.socket.on('user:notifications', (notification) => this.UserNotifications.notification = notification);
+    }
+
+    saveToStorage(cluster = this.connectionSbj.getValue().cluster) {
+        try {
+            localStorage.cluster = JSON.stringify(cluster);
+        }
+        catch (ignore) {
+            // No-op.
+        }
+    }
+
+    updateCluster(newCluster) {
+        const state = this.connectionSbj.getValue();
+
+        const oldCluster = _.find(state.clusters, (cluster) => cluster.id === newCluster.id);
+
+        if (!_.isNil(oldCluster)) {
+            oldCluster.nids = newCluster.nids;
+            oldCluster.addresses = newCluster.addresses;
+            oldCluster.clusterVersion = this.getClusterVersion(newCluster);
+            oldCluster.active = newCluster.active;
+
+            this.connectionSbj.next(state);
+        }
+    }
+
+    switchCluster(cluster) {
+        return Promise.all(_.map([...this.switchClusterListeners], (lnr) => lnr()))
+            .then(() => {
+                const state = this.connectionSbj.getValue();
+
+                state.updateCluster(cluster);
+
+                this.connectionSbj.next(state);
+
+                this.saveToStorage(cluster);
+
+                return Promise.resolve();
+            });
+    }
+
+    /**
+     * @param states
+     * @returns {ng.IPromise}
+     */
+    awaitConnectionState(...states) {
+        const defer = this.$q.defer();
+
+        this.promises.add(defer);
+
+        const subscription = this.connectionSbj.subscribe({
+            next: ({state}) => {
+                if (_.includes(states, state))
+                    defer.resolve();
+            }
+        });
+
+        return defer.promise
+            .finally(() => {
+                subscription.unsubscribe();
+
+                this.promises.delete(defer);
+            });
+    }
+
+    awaitCluster() {
+        return this.awaitConnectionState(State.CONNECTED);
+    }
+
+    awaitAgent() {
+        return this.awaitConnectionState(State.CONNECTED, State.CLUSTER_DISCONNECTED);
+    }
+
+    /**
+     * @param {String} backText
+     * @param {String} [backState]
+     * @returns {ng.IPromise}
+     */
+    startAgentWatch(backText, backState) {
+        this.backText = backText;
+        this.backState = backState;
+
+        const conn = this.connectionSbj.getValue();
+
+        conn.useConnectedCluster();
+
+        this.connectionSbj.next(conn);
+
+        this.modalSubscription && this.modalSubscription.unsubscribe();
+
+        this.modalSubscription = this.connectionSbj.subscribe({
+            next: ({state}) => {
+                switch (state) {
+                    case State.CONNECTED:
+                    case State.CLUSTER_DISCONNECTED:
+                        this.agentModal.hide();
+
+                        break;
+
+                    case State.AGENT_DISCONNECTED:
+                        this.agentModal.agentDisconnected(this.backText, this.backState);
+
+                        break;
+
+                    default:
+                        // Connection to backend is not established yet.
+                }
+            }
+        });
+
+        return this.awaitAgent();
+    }
+
+    stopWatch() {
+        this.modalSubscription && this.modalSubscription.unsubscribe();
+
+        this.promises.forEach((promise) => promise.reject('Agent watch stopped.'));
+    }
+
+    /**
+     *
+     * @param {String} event
+     * @param {Object} [payload]
+     * @returns {ng.IPromise}
+     * @private
+     */
+    _sendToAgent(event, payload = {}) {
+        if (!this.socket)
+            return this.$q.reject('Failed to connect to server');
+
+        const latch = this.$q.defer();
+
+        const onDisconnect = () => {
+            this.socket.removeListener('disconnect', onDisconnect);
+
+            latch.reject('Connection to server was closed');
+        };
+
+        this.socket.on('disconnect', onDisconnect);
+
+        this.socket.emit(event, payload, (err, res) => {
+            this.socket.removeListener('disconnect', onDisconnect);
+
+            if (err)
+                return latch.reject(err);
+
+            latch.resolve(res);
+        });
+
+        return latch.promise;
+    }
+
+    drivers() {
+        return this._sendToAgent('schemaImport:drivers');
+    }
+
+    /**
+     * @param {{jdbcDriverJar: String, jdbcDriverClass: String, jdbcUrl: String, user: String, password: String}}
+     * @returns {ng.IPromise}
+     */
+    schemas({jdbcDriverJar, jdbcDriverClass, jdbcUrl, user, password}) {
+        const info = {user, password};
+
+        return this._sendToAgent('schemaImport:schemas', {jdbcDriverJar, jdbcDriverClass, jdbcUrl, info});
+    }
+
+    /**
+     * @param {{jdbcDriverJar: String, jdbcDriverClass: String, jdbcUrl: String, user: String, password: String, schemas: String, tablesOnly: String}}
+     * @returns {ng.IPromise} Promise on list of tables (see org.apache.ignite.schema.parser.DbTable java class)
+     */
+    tables({jdbcDriverJar, jdbcDriverClass, jdbcUrl, user, password, schemas, tablesOnly}) {
+        const info = {user, password};
+
+        return this._sendToAgent('schemaImport:metadata', {jdbcDriverJar, jdbcDriverClass, jdbcUrl, info, schemas, tablesOnly});
+    }
+
+    /**
+     * @param {Object} cluster
+     * @param {Object} credentials
+     * @param {String} event
+     * @param {Object} params
+     * @returns {ng.IPromise}
+     * @private
+     */
+    _executeOnActiveCluster(cluster, credentials, event, params) {
+        return this._sendToAgent(event, {clusterId: cluster.id, params, credentials})
+            .then((res) => {
+                const {status = SuccessStatus.STATUS_SUCCESS} = res;
+
+                switch (status) {
+                    case SuccessStatus.STATUS_SUCCESS:
+                        if (cluster.secured)
+                            this.clustersSecrets.get(cluster.id).sessionToken = res.sessionToken;
+
+                        if (res.zipped) {
+                            const taskId = _.get(params, 'taskId', '');
+
+                            const useBigIntJson = taskId.startsWith('query');
+
+                            return this.pool.postMessage({payload: res.data, useBigIntJson});
+                        }
+
+                        return res;
+
+                    case SuccessStatus.STATUS_FAILED:
+                        if (res.error.startsWith('Failed to handle request - unknown session token (maybe expired session)')) {
+                            this.clustersSecrets.get(cluster.id).resetSessionToken();
+
+                            return this._executeOnCluster(event, params);
+                        }
+
+                        throw new Error(res.error);
+
+                    case SuccessStatus.AUTH_FAILED:
+                        this.clustersSecrets.get(cluster.id).resetCredentials();
+
+                        throw new Error('Cluster authentication failed. Incorrect user and/or password.');
+
+                    case SuccessStatus.SECURITY_CHECK_FAILED:
+                        throw new Error('Access denied. You are not authorized to access this functionality.');
+
+                    default:
+                        throw new Error('Illegal status in node response');
+                }
+            });
+    }
+
+    /**
+     * @param {String} event
+     * @param {Object} params
+     * @returns {Promise}
+     * @private
+     */
+    _executeOnCluster(event, params) {
+        if (this.isDemoMode())
+            return Promise.resolve(this._executeOnActiveCluster({}, {}, event, params));
+
+        return this.connectionSbj.pipe(first()).toPromise()
+            .then(({cluster}) => {
+                if (_.isNil(cluster))
+                    throw new Error('Failed to execute request on cluster.');
+
+                if (cluster.secured) {
+                    return Promise.resolve(this.clustersSecrets.get(cluster.id))
+                        .then((secrets) => {
+                            if (secrets.hasCredentials())
+                                return secrets;
+
+                            return this.ClusterLoginSrv.askCredentials(secrets)
+                                .then((secrets) => {
+                                    this.clustersSecrets.put(cluster.id, secrets);
+
+                                    return secrets;
+                                });
+                        })
+                        .then((secrets) => ({cluster, credentials: secrets.getCredentials()}));
+                }
+
+                return {cluster, credentials: {}};
+            })
+            .then(({cluster, credentials}) => this._executeOnActiveCluster(cluster, credentials, event, params))
+            .catch((err) => {
+                if (err instanceof CancellationError)
+                    return;
+
+                throw err;
+            });
+    }
+
+    /**
+     * @param {boolean} [attr] Collect node attributes.
+     * @param {boolean} [mtr] Collect node metrics.
+     * @param {boolean} [caches] Collect node caches descriptors.
+     * @returns {Promise}
+     */
+    topology(attr = false, mtr = false, caches = false) {
+        return this._executeOnCluster('node:rest', {cmd: 'top', attr, mtr, caches});
+    }
+
+    collectCacheNames(nid) {
+        if (this.available(COLLECT_BY_CACHE_GROUPS_SINCE))
+            return this.visorTask('cacheNamesCollectorTask', nid);
+
+        return Promise.resolve({cacheGroupsNotAvailable: true});
+    }
+
+    publicCacheNames() {
+        return this.collectCacheNames()
+            .then((data) => {
+                if (nonEmpty(data.caches))
+                    return _.difference(_.keys(data.caches), RESERVED_CACHE_NAMES);
+
+                return this.topology(false, false, true)
+                    .then((nodes) => {
+                        return _.map(_.uniqBy(_.flatMap(nodes, 'caches'), 'name'), 'name');
+                    });
+            });
+    }
+
+    /**
+     * @param {string} cacheName Cache name.
+     */
+    cacheNodes(cacheName) {
+        if (this.available(IGNITE_2_0))
+            return this.visorTask('cacheNodesTaskX2', null, cacheName);
+
+        return this.visorTask('cacheNodesTask', null, cacheName);
+    }
+
+    /**
+     * @returns {Promise}
+     */
+    metadata() {
+        return this._executeOnCluster('node:rest', {cmd: 'metadata'})
+            .then((caches) => {
+                let types = [];
+
+                const _compact = (className) => {
+                    return className.replace('java.lang.', '').replace('java.util.', '').replace('java.sql.', '');
+                };
+
+                const _typeMapper = (meta, typeName) => {
+                    const maskedName = _.isEmpty(meta.cacheName) ? '<default>' : meta.cacheName;
+
+                    let fields = meta.fields[typeName];
+
+                    let columns = [];
+
+                    for (const fieldName in fields) {
+                        if (fields.hasOwnProperty(fieldName)) {
+                            const fieldClass = _compact(fields[fieldName]);
+
+                            columns.push({
+                                type: 'field',
+                                name: fieldName,
+                                clazz: fieldClass,
+                                system: fieldName === '_KEY' || fieldName === '_VAL',
+                                cacheName: meta.cacheName,
+                                typeName,
+                                maskedName
+                            });
+                        }
+                    }
+
+                    const indexes = [];
+
+                    for (const index of meta.indexes[typeName]) {
+                        fields = [];
+
+                        for (const field of index.fields) {
+                            fields.push({
+                                type: 'index-field',
+                                name: field,
+                                order: index.descendings.indexOf(field) < 0,
+                                unique: index.unique,
+                                cacheName: meta.cacheName,
+                                typeName,
+                                maskedName
+                            });
+                        }
+
+                        if (fields.length > 0) {
+                            indexes.push({
+                                type: 'index',
+                                name: index.name,
+                                children: fields,
+                                cacheName: meta.cacheName,
+                                typeName,
+                                maskedName
+                            });
+                        }
+                    }
+
+                    columns = _.sortBy(columns, 'name');
+
+                    if (nonEmpty(indexes)) {
+                        columns = columns.concat({
+                            type: 'indexes',
+                            name: 'Indexes',
+                            cacheName: meta.cacheName,
+                            typeName,
+                            maskedName,
+                            children: indexes
+                        });
+                    }
+
+                    return {
+                        type: 'type',
+                        cacheName: meta.cacheName || '',
+                        typeName,
+                        maskedName,
+                        children: columns
+                    };
+                };
+
+                for (const meta of caches) {
+                    const cacheTypes = meta.types.map(_typeMapper.bind(null, meta));
+
+                    if (!_.isEmpty(cacheTypes))
+                        types = types.concat(cacheTypes);
+                }
+
+                return types;
+            });
+    }
+
+    /**
+     * @param {String} taskId
+     * @param {Array.<String>|String} nids
+     * @param {Array.<Object>} args
+     */
+    visorTask(taskId, nids, ...args) {
+        args = _.map(args, (arg) => maskNull(arg));
+
+        nids = _.isArray(nids) ? nids.join(';') : maskNull(nids);
+
+        return this._executeOnCluster('node:visor', {taskId, nids, args});
+    }
+
+    /**
+     * @param {String} nid Node id.
+     * @param {String} cacheName Cache name.
+     * @param {String} [query] Query if null then scan query.
+     * @param {Boolean} nonCollocatedJoins Flag whether to execute non collocated joins.
+     * @param {Boolean} enforceJoinOrder Flag whether enforce join order is enabled.
+     * @param {Boolean} replicatedOnly Flag whether query contains only replicated tables.
+     * @param {Boolean} local Flag whether to execute query locally.
+     * @param {Number} pageSize
+     * @param {Boolean} [lazy] query flag.
+     * @param {Boolean} [collocated] Collocated query.
+     * @returns {Promise.<VisorQueryResult>} Query execution result.
+     */
+    querySql({nid, cacheName, query, nonCollocatedJoins, enforceJoinOrder, replicatedOnly, local, pageSize, lazy = false, collocated = false}) {
+        if (this.available(IGNITE_2_0)) {
+            let args = [cacheName, query, nonCollocatedJoins, enforceJoinOrder, replicatedOnly, local, pageSize];
+
+            if (this.available(...COLLOCATED_QUERY_SINCE))
+                args = [...args, lazy, collocated];
+            else if (this.available(...LAZY_QUERY_SINCE))
+                args = [...args, lazy];
+
+            return this.visorTask('querySqlX2', nid, ...args).then(({error, result}) => {
+                if (_.isEmpty(error))
+                    return result;
+
+                return Promise.reject(error);
+            });
+        }
+
+        cacheName = _.isEmpty(cacheName) ? null : cacheName;
+
+        let queryPromise;
+
+        if (enforceJoinOrder)
+            queryPromise = this.visorTask('querySqlV3', nid, cacheName, query, nonCollocatedJoins, enforceJoinOrder, local, pageSize);
+        else if (nonCollocatedJoins)
+            queryPromise = this.visorTask('querySqlV2', nid, cacheName, query, nonCollocatedJoins, local, pageSize);
+        else
+            queryPromise = this.visorTask('querySql', nid, cacheName, query, local, pageSize);
+
+        return queryPromise
+            .then(({key, value}) => {
+                if (_.isEmpty(key))
+                    return value;
+
+                return Promise.reject(key);
+            });
+    }
+
+    /**
+     * @param {String} nid Node id.
+     * @param {String} queryId Query ID.
+     * @param {Number} pageSize
+     * @returns {Promise.<VisorQueryResult>} Query execution result.
+     */
+    queryFetchFistsPage(nid, queryId, pageSize) {
+        return this.visorTask('queryFetchFirstPage', nid, queryId, pageSize).then(({error, result}) => {
+            if (_.isEmpty(error))
+                return result;
+
+            return Promise.reject(error);
+        });
+    }
+
+    /**
+     * @param {String} nid Node id.
+     * @param {String} queryId Query ID.
+     * @returns {Promise.<VisorQueryPingResult>} Query execution result.
+     */
+    queryPing(nid, queryId) {
+        if (this.available(...QUERY_PING_SINCE)) {
+            return this.visorTask('queryPing', nid, queryId, 1).then(({error, result}) => {
+                if (_.isEmpty(error))
+                    return {queryPingSupported: true};
+
+                return Promise.reject(error);
+            });
+        }
+
+        return Promise.resolve({queryPingSupported: false});
+    }
+
+    /**
+     * @param {String} nid Node id.
+     * @param {Number} queryId
+     * @param {Number} pageSize
+     * @returns {Promise.<VisorQueryResult>} Query execution result.
+     */
+    queryNextPage(nid, queryId, pageSize) {
+        if (this.available(IGNITE_2_0))
+            return this.visorTask('queryFetchX2', nid, queryId, pageSize);
+
+        return this.visorTask('queryFetch', nid, queryId, pageSize);
+    }
+
+    /**
+     * @param {String} nid Node id.
+     * @param {Number} [queryId]
+     * @returns {Promise<Void>}
+     */
+    queryClose(nid, queryId) {
+        if (this.available(IGNITE_2_0)) {
+            return this.visorTask('queryCloseX2', nid, 'java.util.Map', 'java.util.UUID', 'java.util.Collection',
+                nid + '=' + queryId);
+        }
+
+        return this.visorTask('queryClose', nid, nid, queryId);
+    }
+
+    /**
+     * @param {String} nid Node id.
+     * @param {String} cacheName Cache name.
+     * @param {String} filter Filter text.
+     * @param {Boolean} regEx Flag whether filter by regexp.
+     * @param {Boolean} caseSensitive Case sensitive filtration.
+     * @param {Boolean} near Scan near cache.
+     * @param {Boolean} local Flag whether to execute query locally.
+     * @param {Number} pageSize Page size.
+     * @returns {Promise.<VisorQueryResult>} Query execution result.
+     */
+    queryScan({nid, cacheName, filter, regEx, caseSensitive, near, local, pageSize}) {
+        if (this.available(IGNITE_2_0)) {
+            return this.visorTask('queryScanX2', nid, cacheName, filter, regEx, caseSensitive, near, local, pageSize)
+                .then(({error, result}) => {
+                    if (_.isEmpty(error))
+                        return result;
+
+                    return Promise.reject(error);
+                });
+        }
+
+        /** Prefix for node local key for SCAN near queries. */
+        const SCAN_CACHE_WITH_FILTER = 'VISOR_SCAN_CACHE_WITH_FILTER';
+
+        /** Prefix for node local key for SCAN near queries. */
+        const SCAN_CACHE_WITH_FILTER_CASE_SENSITIVE = 'VISOR_SCAN_CACHE_WITH_FILTER_CASE_SENSITIVE';
+
+        const prefix = caseSensitive ? SCAN_CACHE_WITH_FILTER_CASE_SENSITIVE : SCAN_CACHE_WITH_FILTER;
+        const query = `${prefix}${filter}`;
+
+        return this.querySql({nid, cacheName, query, nonCollocatedJoins: false, enforceJoinOrder: false, replicatedOnly: false, local, pageSize});
+    }
+
+    /**
+     * Change cluster active state.
+     *
+     * @returns {Promise}
+     */
+    toggleClusterState() {
+        const { cluster } = this.connectionSbj.getValue();
+        const active = !cluster.active;
+
+        return this.visorTask('toggleClusterState', null, active)
+            .then(() => this.updateCluster({ ...cluster, active }));
+    }
+
+    hasCredentials(clusterId) {
+        return this.clustersSecrets.get(clusterId).hasCredentials();
+    }
+}
diff --git a/modules/frontend/app/modules/agent/AgentModal.service.js b/modules/frontend/app/modules/agent/AgentModal.service.js
new file mode 100644
index 0000000..e4b41ac
--- /dev/null
+++ b/modules/frontend/app/modules/agent/AgentModal.service.js
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import _ from 'lodash';
+import templateUrl from 'views/templates/agent-download.tpl.pug';
+
+export default class AgentModal {
+    static $inject = ['$rootScope', '$state', '$modal', 'IgniteMessages'];
+
+    /**
+     * @param {ng.IRootScopeService} $root
+     * @param {import('@uirouter/angularjs').StateService} $state
+     * @param {mgcrea.ngStrap.modal.IModalService} $modal
+     * @param {ReturnType<typeof import('app/services/Messages.service').default>} Messages
+     */
+    constructor($root, $state, $modal, Messages) {
+        const self = this;
+
+        this.$root = $root;
+        this.$state = $state;
+        this.Messages = Messages;
+
+        // Pre-fetch modal dialogs.
+        this.modal = $modal({
+            templateUrl,
+            show: false,
+            backdrop: 'static',
+            keyboard: false,
+            controller() { return self;},
+            controllerAs: 'ctrl'
+        });
+
+        $root.$on('user', (event, user) => this.user = user);
+    }
+
+    hide() {
+        this.modal.hide();
+    }
+
+    /**
+     * Close dialog and go by specified link.
+     */
+    back() {
+        this.Messages.hideAlert();
+
+        this.hide();
+
+        _.forEach(angular.element('.modal'), (m) => angular.element(m).scope().$hide());
+
+        if (this.backState)
+            this.$state.go(this.backState);
+    }
+
+    /**
+     * @param {String} backState
+     * @param {String} [backText]
+     */
+    agentDisconnected(backText, backState) {
+        this.backText = backText;
+        this.backState = backState;
+
+        this.status = 'agentMissing';
+
+        this.modal.$promise.then(() => this.modal.show());
+    }
+
+    /**
+     * @param {String} backState
+     * @param {String} [backText]
+     */
+    clusterDisconnected(backText, backState) {
+        this.backText = backText;
+        this.backState = backState;
+
+        this.status = 'nodeMissing';
+
+        this.modal.$promise.then(() => this.modal.show());
+    }
+
+    get securityToken() {
+        return this.$root.user.becameToken || this.$root.user.token;
+    }
+}
diff --git a/modules/frontend/app/modules/agent/agent.module.js b/modules/frontend/app/modules/agent/agent.module.js
new file mode 100644
index 0000000..9189d92
--- /dev/null
+++ b/modules/frontend/app/modules/agent/agent.module.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import AgentModal from './AgentModal.service';
+import AgentManager from './AgentManager.service';
+
+import clusterLogin from './components/cluster-login';
+
+angular
+    .module('ignite-console.agent', [
+        clusterLogin.name
+    ])
+    .service('AgentModal', AgentModal)
+    .service('AgentManager', AgentManager);
diff --git a/modules/frontend/app/modules/agent/components/cluster-login/component.js b/modules/frontend/app/modules/agent/components/cluster-login/component.js
new file mode 100644
index 0000000..e932bad
--- /dev/null
+++ b/modules/frontend/app/modules/agent/components/cluster-login/component.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import {ClusterSecrets} from '../../types/ClusterSecrets';
+
+export const component = {
+    bindings: {
+        secrets: '=',
+        onLogin: '&',
+        onHide: '&'
+    },
+    controller: class {
+        /** @type {ClusterSecrets} */
+        secrets;
+        /** @type {ng.ICompiledExpression} */
+        onLogin;
+        /** @type {ng.ICompiledExpression} */
+        onHide;
+        /** @type {ng.IFormController} */
+        form;
+
+        login() {
+            if (this.form.$invalid)
+                return;
+
+            this.onLogin();
+        }
+    },
+    template
+};
diff --git a/modules/frontend/app/modules/agent/components/cluster-login/index.js b/modules/frontend/app/modules/agent/components/cluster-login/index.js
new file mode 100644
index 0000000..177ccda
--- /dev/null
+++ b/modules/frontend/app/modules/agent/components/cluster-login/index.js
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import {component} from './component';
+import service from './service';
+
+export default angular
+    .module('ignite-console.agent.cluster-login', [])
+    .service('ClusterLoginService', service)
+    .component('clusterLogin', component);
diff --git a/modules/frontend/app/modules/agent/components/cluster-login/service.js b/modules/frontend/app/modules/agent/components/cluster-login/service.js
new file mode 100644
index 0000000..406152d
--- /dev/null
+++ b/modules/frontend/app/modules/agent/components/cluster-login/service.js
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+import {CancellationError} from 'app/errors/CancellationError';
+
+export default class ClusterLoginService {
+    static $inject = ['$modal', '$q'];
+
+    deferred;
+
+    /**
+     * @param {mgcrea.ngStrap.modal.IModalService} $modal
+     * @param {ng.IQService} $q
+     */
+    constructor($modal, $q) {
+        this.$modal = $modal;
+        this.$q = $q;
+    }
+
+    /**
+     * @param {import('../../types/ClusterSecrets').ClusterSecrets} baseSecrets
+     * @returns {ng.IPromise<import('../../types/ClusterSecrets').ClusterSecrets>}
+     */
+    askCredentials(baseSecrets) {
+        if (this.deferred)
+            return this.deferred.promise;
+
+        this.deferred = this.$q.defer();
+
+        const self = this;
+
+        const modal = this.$modal({
+            template: `
+                <cluster-login
+                    secrets='$ctrl.secrets'
+                    on-login='$ctrl.onLogin()'
+                    on-hide='$ctrl.onHide()'
+                ></cluster-login>
+            `,
+            controller: [function() {
+                this.secrets = _.clone(baseSecrets);
+
+                this.onLogin = () => {
+                    self.deferred.resolve(this.secrets);
+                };
+
+                this.onHide = () => {
+                    self.deferred.reject(new CancellationError());
+                };
+            }],
+            controllerAs: '$ctrl',
+            backdrop: 'static',
+            show: true
+        });
+
+        return modal.$promise
+            .then(() => this.deferred.promise)
+            .finally(() => {
+                this.deferred = null;
+
+                modal.hide();
+            });
+    }
+
+    cancel() {
+        if (this.deferred)
+            this.deferred.reject(new CancellationError());
+    }
+}
diff --git a/modules/frontend/app/modules/agent/components/cluster-login/template.pug b/modules/frontend/app/modules/agent/components/cluster-login/template.pug
new file mode 100644
index 0000000..19ab6f4
--- /dev/null
+++ b/modules/frontend/app/modules/agent/components/cluster-login/template.pug
@@ -0,0 +1,58 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+.modal.modal--ignite.theme--ignite(tabindex='-1' role='dialog')
+    .modal-dialog
+       -var form = '$ctrl.form'
+
+       form.modal-content(name=form novalidate ng-submit='$ctrl.login()')
+            .modal-header
+                h4.modal-title
+                    span Cluster Authentication
+                button.close(type='button' aria-label='Close' ng-click='$ctrl.onHide()')
+                    svg(ignite-icon="cross")
+            .modal-body
+                p Enter your credentials to access the cluster.
+                .row
+                    .col-50
+                        +form-field__text({
+                            label: 'User:',
+                            model: '$ctrl.secrets.user',
+                            name: '"user"',
+                            placeholder: 'Enter user',
+                            required: true
+                        })(
+                            ng-model-options='{allowInvalid: true}'
+                            autocomplete='node-user'
+                            ignite-auto-focus
+                        )
+                    .col-50
+                        +form-field__password({
+                            label: 'Password:',
+                            model: '$ctrl.secrets.password',
+                            name: '"password"',
+                            placeholder: 'Enter password',
+                            required: true
+                        })(
+                            autocomplete='node-password'
+                        )
+            .modal-footer
+                div
+                    button#btn-cancel.btn-ignite.btn-ignite--link-success(type='button' ng-click='$ctrl.onHide()') Cancel
+                    button#btn-login.btn-ignite.btn-ignite--success Login
+
diff --git a/modules/frontend/app/modules/agent/decompress.worker.js b/modules/frontend/app/modules/agent/decompress.worker.js
new file mode 100644
index 0000000..deffca3
--- /dev/null
+++ b/modules/frontend/app/modules/agent/decompress.worker.js
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+import pako from 'pako';
+import bigIntJSON from 'json-bigint';
+
+/** This worker decode & decompress BASE64/Zipped data and parse to JSON. */
+// eslint-disable-next-line no-undef
+onmessage = function(e) {
+    const data = e.data;
+
+    const binaryString = atob(data.payload); // Decode from BASE64
+
+    const unzipped = pako.inflate(binaryString, {to: 'string'});
+
+    const res = data.useBigIntJson
+        ? bigIntJSON({storeAsString: true}).parse(unzipped)
+        : JSON.parse(unzipped);
+
+    postMessage(_.get(res, 'result', res));
+};
diff --git a/modules/frontend/app/modules/agent/types/Cluster.js b/modules/frontend/app/modules/agent/types/Cluster.js
new file mode 100644
index 0000000..efb303d
--- /dev/null
+++ b/modules/frontend/app/modules/agent/types/Cluster.js
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class Cluster {
+    /** @type {String} */
+    id;
+
+    /** @type {String} */
+    name;
+
+    /** @type {Boolean} */
+    connected = true;
+
+    /** @type {Boolean} */
+    secured;
+
+    constructor({id, name, secured = false}) {
+        this.id = id;
+        this.name = name;
+        this.secured = secured;
+    }
+}
+
diff --git a/modules/frontend/app/modules/agent/types/ClusterSecrets.js b/modules/frontend/app/modules/agent/types/ClusterSecrets.js
new file mode 100644
index 0000000..edff351
--- /dev/null
+++ b/modules/frontend/app/modules/agent/types/ClusterSecrets.js
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {nonEmpty} from 'app/utils/lodashMixins';
+
+export class ClusterSecrets {
+    /** @type {String} */
+    user;
+
+    /** @type {String} */
+    password;
+
+    /** @type {String} */
+    sessionToken;
+
+    constructor() {
+        this.user = 'ignite';
+    }
+
+    hasCredentials() {
+        return nonEmpty(this.user) && nonEmpty(this.password);
+    }
+
+    resetCredentials() {
+        this.resetSessionToken();
+
+        this.password = null;
+    }
+
+    resetSessionToken() {
+        this.sessionToken = null;
+    }
+
+    /**
+     * @return {{sessionToken: String}|{'user': String, 'password': String}}
+     */
+    getCredentials() {
+        const { sessionToken } = this;
+
+        if (sessionToken)
+            return { sessionToken };
+
+        const { user, password } = this;
+
+        return { user, password };
+    }
+}
diff --git a/modules/frontend/app/modules/agent/types/ClusterSecretsManager.js b/modules/frontend/app/modules/agent/types/ClusterSecretsManager.js
new file mode 100644
index 0000000..f5bd5ac
--- /dev/null
+++ b/modules/frontend/app/modules/agent/types/ClusterSecretsManager.js
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {ClusterSecrets} from './ClusterSecrets';
+
+export class ClusterSecretsManager {
+    /** @type {Map<String, ClusterSecrets>} */
+    memoryCache = new Map();
+
+    /**
+     * @param {String} clusterId
+     * @private
+     */
+    _has(clusterId) {
+        return this.memoryCache.has(clusterId);
+    }
+
+    /**
+     * @param {String} clusterId
+     * @private
+     */
+    _get(clusterId) {
+        return this.memoryCache.get(clusterId);
+    }
+
+    /**
+     * @param {String} clusterId
+     */
+    get(clusterId) {
+        if (this._has(clusterId))
+            return this._get(clusterId);
+
+        const secrets = new ClusterSecrets();
+
+        this.put(clusterId, secrets);
+
+        return secrets;
+    }
+
+    /**
+     * @param {String} clusterId
+     * @param {ClusterSecrets} secrets
+     */
+    put(clusterId, secrets) {
+        this.memoryCache.set(clusterId, secrets);
+    }
+
+    /**
+     * @param {String} clusterId
+     */
+    reset(clusterId) {
+        const secrets = this._get(clusterId);
+
+        secrets && secrets.resetCredentials();
+    }
+}
diff --git a/modules/frontend/app/modules/branding/branding.module.js b/modules/frontend/app/modules/branding/branding.module.js
new file mode 100644
index 0000000..8b23d04
--- /dev/null
+++ b/modules/frontend/app/modules/branding/branding.module.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import IgniteBranding from './branding.service';
+
+import igniteTerms from './terms.directive';
+import igniteFeatures from './features.directive';
+
+angular
+.module('ignite-console.branding', [
+    'tf.metatags'
+])
+.service('IgniteBranding', IgniteBranding)
+.config(['tfMetaTagsProvider', (tfMetaTagsProvider) => {
+    tfMetaTagsProvider.setDefaults({
+        title: 'Apache Ignite - Management Tool and Configuration Wizard',
+        properties: {
+            description: 'The Apache Ignite Web Console is an interactive management tool and configuration wizard which walks you through the creation of config files. Try it now.'
+        }
+    });
+
+    tfMetaTagsProvider.setTitleSuffix(' – Apache Ignite Web Console');
+}])
+.directive('igniteTerms', igniteTerms)
+.directive('igniteFeatures', igniteFeatures);
diff --git a/modules/frontend/app/modules/branding/branding.service.js b/modules/frontend/app/modules/branding/branding.service.js
new file mode 100644
index 0000000..aac0acb
--- /dev/null
+++ b/modules/frontend/app/modules/branding/branding.service.js
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class {
+    static $inject = ['IgniteVersion'];
+
+    /**
+     * @param {import('app/services/Version.service').default} Version
+     */
+    constructor(Version) {
+        this.titleSuffix = ' - Apache Ignite Web Console';
+
+        this.showIgniteLogo = false;
+
+        this.footerHtml = [
+            '<p>Apache Ignite Web Console (${Version.webConsole})</p>',
+            '<p>© 2020 The Apache Software Foundation.</p>',
+            '<p>Apache, Apache Ignite, the Apache feather and the Apache Ignite logo are trademarks of The Apache Software Foundation.</p>'
+        ].join('\n');
+
+        this.termsState = null;
+
+        this.featuresHtml = [
+            '<p>Web Console is an interactive management tool which allows to:</p>',
+            '<ul>',
+            '   <li>Create and download cluster configurations</li>',
+            '   <li>Automatically import domain model from any RDBMS</li>',
+            '   <li>Connect to cluster and run SQL analytics on it</li>',
+            '</ul>'
+        ].join('\n');
+    }
+}
diff --git a/modules/frontend/app/modules/branding/features.directive.js b/modules/frontend/app/modules/branding/features.directive.js
new file mode 100644
index 0000000..39c639c
--- /dev/null
+++ b/modules/frontend/app/modules/branding/features.directive.js
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const template = '<div class="features" ng-bind-html="features.html"></div>';
+
+/**
+ * @param {import('./branding.service').default} branding
+ */
+export default function factory(branding) {
+    function controller() {
+        const ctrl = this;
+
+        ctrl.html = branding.featuresHtml;
+    }
+
+    return {
+        restrict: 'E',
+        template,
+        controller,
+        controllerAs: 'features',
+        replace: true
+    };
+}
+
+factory.$inject = ['IgniteBranding'];
+
diff --git a/modules/frontend/app/modules/branding/terms.directive.js b/modules/frontend/app/modules/branding/terms.directive.js
new file mode 100644
index 0000000..8126a39
--- /dev/null
+++ b/modules/frontend/app/modules/branding/terms.directive.js
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @param {import('./branding.service').default} branding
+ */
+export default function factory(branding) {
+    function controller() {
+        const ctrl = this;
+
+        ctrl.termsState = branding.termsState;
+    }
+
+    return {
+        restrict: 'A',
+        controller,
+        controllerAs: 'terms'
+    };
+}
+
+factory.$inject = ['IgniteBranding'];
diff --git a/modules/frontend/app/modules/cluster/Cache.js b/modules/frontend/app/modules/cluster/Cache.js
new file mode 100644
index 0000000..c5b1d00
--- /dev/null
+++ b/modules/frontend/app/modules/cluster/Cache.js
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class Cache {
+    constructor(cache) {
+        this.dynamicDeploymentId = cache.dynamicDeploymentId;
+
+        // Name.
+        this.name = cache.name;
+
+        // Mode.
+        this.mode = cache.mode;
+
+        // Heap.
+        this.size = cache.size;
+        this.primarySize = cache.primarySize;
+        this.backupSize = _.isNil(cache.backupSize) ? cache.dhtSize - cache.primarySize : cache.backupSize;
+        this.nearSize = cache.nearSize;
+
+        const m = cache.metrics;
+
+        // Off-heap.
+        this.offHeapAllocatedSize = m.offHeapAllocatedSize;
+        this.offHeapSize = m.offHeapEntriesCount;
+        this.offHeapPrimarySize = m.offHeapPrimaryEntriesCount || 0;
+        this.offHeapBackupSize = this.offHeapSize - this.offHeapPrimarySize;
+
+        // Read/write metrics.
+        this.hits = m.hits;
+        this.misses = m.misses;
+        this.reads = m.reads;
+        this.writes = m.writes;
+
+        // Transaction metrics.
+        this.commits = m.txCommits;
+        this.rollbacks = m.txRollbacks;
+    }
+}
diff --git a/modules/frontend/app/modules/cluster/CacheMetrics.js b/modules/frontend/app/modules/cluster/CacheMetrics.js
new file mode 100644
index 0000000..609b181
--- /dev/null
+++ b/modules/frontend/app/modules/cluster/CacheMetrics.js
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class CacheMetrics {
+    constructor(cache) {
+        this.dynamicDeploymentId = cache.dynamicDeploymentId;
+
+        // Name.
+        this.name = cache.name;
+
+        // Mode.
+        this.mode = cache.mode;
+
+        // Memory Usage.
+        this.memorySize = cache.memorySize;
+
+        // Heap.
+        this.size = cache.size;
+        this.primarySize = cache.primarySize;
+        this.backupSize = cache.dhtSize - cache.primarySize;
+        this.nearSize = cache.nearSize;
+
+        // Off-heap.
+        this.offHeapAllocatedSize = cache.offHeapAllocatedSize;
+        this.offHeapSize = cache.offHeapEntriesCount;
+        this.offHeapPrimarySize = cache.offHeapPrimaryEntriesCount || 0;
+        this.offHeapBackupSize = cache.offHeapBackupEntriesCount || 0;
+
+        // Swap.
+        this.swapSize = cache.swapSize;
+        this.swapKeys = cache.swapKeys;
+
+        const m = cache.metrics;
+
+        // Read/write metrics.
+        this.hits = m.hits;
+        this.misses = m.misses;
+        this.reads = m.reads;
+        this.writes = m.writes;
+
+        // Transaction metrics.
+        this.commits = m.txCommits;
+        this.rollbacks = m.txRollbacks;
+
+        // Admin metrics.
+        this.statisticsEnabled = m.statisticsEnabled;
+    }
+}
diff --git a/modules/frontend/app/modules/cluster/NodeMetrics.js b/modules/frontend/app/modules/cluster/NodeMetrics.js
new file mode 100644
index 0000000..400d480
--- /dev/null
+++ b/modules/frontend/app/modules/cluster/NodeMetrics.js
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class NodeMetrics {
+}
diff --git a/modules/frontend/app/modules/demo/Demo.module.js b/modules/frontend/app/modules/demo/Demo.module.js
new file mode 100644
index 0000000..57f3a02
--- /dev/null
+++ b/modules/frontend/app/modules/demo/Demo.module.js
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import DEMO_INFO from 'app/data/demo-info.json';
+import templateUrl from 'views/templates/demo-info.tpl.pug';
+
+const DEMO_QUERY_STATE = {state: 'base.sql.notebook', params: {noteId: 'demo'}};
+
+/**
+ * @param {import('@uirouter/angularjs').StateProvider} $state
+ * @param {ng.IHttpProvider} $http
+ */
+export function DemoProvider($state, $http) {
+    if (/(\/demo.*)/ig.test(location.pathname))
+        sessionStorage.setItem('IgniteDemoMode', 'true');
+
+    const enabled = sessionStorage.getItem('IgniteDemoMode') === 'true';
+
+    if (enabled)
+        $http.interceptors.push('demoInterceptor');
+
+    function service($root) {
+        $root.IgniteDemoMode = enabled;
+
+        return {enabled};
+    }
+    service.$inject = ['$rootScope'];
+
+    this.$get = service;
+    return this;
+}
+
+DemoProvider.$inject = ['$stateProvider', '$httpProvider'];
+
+/**
+ * @param {{enabled: boolean}} Demo
+ * @returns {ng.IHttpInterceptor}
+ */
+function demoInterceptor(Demo) {
+    const isApiRequest = (url) => /\/api\/v1/ig.test(url);
+
+    return {
+        request(cfg) {
+            if (Demo.enabled && isApiRequest(cfg.url))
+                cfg.headers.IgniteDemoMode = true;
+
+            return cfg;
+        }
+    };
+}
+
+demoInterceptor.$inject = ['Demo'];
+
+
+
+function igniteDemoInfoProvider() {
+    const items = DEMO_INFO;
+
+    this.update = (data) => items[0] = data;
+
+    this.$get = () => {
+        return items;
+    };
+    return this;
+}
+
+/**
+ * @param {ng.IRootScopeService} $rootScope
+ * @param {mgcrea.ngStrap.modal.IModalScope} $modal
+ * @param {import('@uirouter/angularjs').StateService} $state
+ * @param {ng.IQService} $q
+ * @param {Array<{title: string, message: Array<string>}>} igniteDemoInfo
+ * @param {import('app/modules/agent/AgentManager.service').default} agentMgr
+ */
+function DemoInfo($rootScope, $modal, $state, $q, igniteDemoInfo, agentMgr) {
+    const scope = $rootScope.$new();
+
+    let closePromise = null;
+
+    function _fillPage() {
+        const model = igniteDemoInfo;
+
+        scope.title = model[0].title;
+        scope.message = model[0].message.join(' ');
+    }
+
+    const dialog = $modal({
+        templateUrl,
+        scope,
+        show: false,
+        backdrop: 'static'
+    });
+
+    scope.downloadAgentHref = '/api/v1/downloads/agent';
+
+    scope.close = () => {
+        dialog.hide();
+
+        closePromise && closePromise.resolve();
+    };
+
+    return {
+        show: () => {
+            closePromise = $q.defer();
+
+            _fillPage();
+
+            return dialog.$promise
+                .then(dialog.show)
+                .then(() => Promise.race([agentMgr.awaitCluster(), closePromise.promise]))
+                .then(() => scope.hasAgents = true);
+        }
+    };
+}
+
+DemoInfo.$inject = ['$rootScope', '$modal', '$state', '$q', 'igniteDemoInfo', 'AgentManager'];
+
+/**
+ * @param {import('@uirouter/angularjs').StateProvider} $stateProvider
+ */
+function config($stateProvider) {
+    $stateProvider
+        .state('demo', {
+            abstract: true,
+            url: '/demo',
+            template: '<ui-view></ui-view>'
+        })
+        .state('demo.resume', {
+            url: '/resume',
+            permission: 'demo',
+            redirectTo: DEMO_QUERY_STATE,
+            unsaved: true,
+            tfMetaTags: {
+                title: 'Demo resume'
+            }
+        })
+        .state('demo.reset', {
+            url: '/reset',
+            permission: 'demo',
+            redirectTo: (trans) => {
+                const $http = trans.injector().get('$http');
+
+                return $http.post('/api/v1/demo/reset')
+                    .then(() => DEMO_QUERY_STATE)
+                    .catch((err) => {
+                        trans.injector().get('IgniteMessages').showError(err);
+
+                        return DEMO_QUERY_STATE;
+                    });
+            },
+            unsaved: true,
+            tfMetaTags: {
+                title: 'Demo reset'
+            }
+        });
+}
+
+config.$inject = ['$stateProvider'];
+
+angular
+    .module('ignite-console.demo', [])
+    .config(config)
+    .provider('Demo', DemoProvider)
+    .factory('demoInterceptor', demoInterceptor)
+    .provider('igniteDemoInfo', igniteDemoInfoProvider)
+    .service('DemoInfo', DemoInfo);
diff --git a/modules/frontend/app/modules/dialog/dialog-content.directive.js b/modules/frontend/app/modules/dialog/dialog-content.directive.js
new file mode 100644
index 0000000..fb5d972
--- /dev/null
+++ b/modules/frontend/app/modules/dialog/dialog-content.directive.js
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default () => {
+    const link = ($scope, $element, $attrs, igniteDialog) => {
+        igniteDialog.content = $element.html();
+
+        $element.hide();
+    };
+
+    return {
+        scope: {},
+        restrict: 'E',
+        link,
+        require: '^igniteDialog'
+    };
+};
diff --git a/modules/frontend/app/modules/dialog/dialog-title.directive.js b/modules/frontend/app/modules/dialog/dialog-title.directive.js
new file mode 100644
index 0000000..ebd2d94
--- /dev/null
+++ b/modules/frontend/app/modules/dialog/dialog-title.directive.js
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default () => {
+    const link = ($scope, $element, $attrs, igniteDialog) => {
+        igniteDialog.title = $element.text();
+
+        $element.hide();
+    };
+
+    return {
+        scope: {},
+        restrict: 'E',
+        link,
+        require: '^igniteDialog'
+    };
+};
diff --git a/modules/frontend/app/modules/dialog/dialog.controller.js b/modules/frontend/app/modules/dialog/dialog.controller.js
new file mode 100644
index 0000000..a75ff1f
--- /dev/null
+++ b/modules/frontend/app/modules/dialog/dialog.controller.js
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default ['$rootScope', '$scope', 'IgniteDialog', function($root, $scope, IgniteDialog) {
+    const ctrl = this;
+
+    this.$onInit = () => {
+        const dialog = new IgniteDialog({
+            scope: $scope
+        });
+
+        ctrl.show = () => {
+            dialog.$promise.then(dialog.show);
+        };
+
+        $scope.$watch(() => ctrl.title, () => {
+            $scope.title = ctrl.title;
+        });
+
+        $scope.$watch(() => ctrl.content, () => {
+            $scope.content = ctrl.content;
+        });
+    };
+}];
diff --git a/modules/frontend/app/modules/dialog/dialog.directive.js b/modules/frontend/app/modules/dialog/dialog.directive.js
new file mode 100644
index 0000000..96a96e2
--- /dev/null
+++ b/modules/frontend/app/modules/dialog/dialog.directive.js
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import controller from './dialog.controller';
+
+const template = '<a ng-click="ctrl.show()"><span ng-transclude=""></span></a>';
+
+export default () => {
+    return {
+        restrict: 'E',
+        template,
+        controller,
+        controllerAs: 'ctrl',
+        replace: true,
+        transclude: true,
+        require: '^igniteDialog'
+    };
+};
diff --git a/modules/frontend/app/modules/dialog/dialog.factory.js b/modules/frontend/app/modules/dialog/dialog.factory.js
new file mode 100644
index 0000000..14afcdf
--- /dev/null
+++ b/modules/frontend/app/modules/dialog/dialog.factory.js
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import templateUrl from './dialog.tpl.pug';
+
+/**
+ * @param {mgcrea.ngStrap.modal.IModalService} $modal
+ */
+export default function factory($modal) {
+    const defaults = {
+        templateUrl,
+        show: false
+    };
+
+    return function(options) {
+        options = _.extend({}, defaults, options);
+
+        return $modal(options);
+    };
+}
+
+factory.$inject = ['$modal'];
diff --git a/modules/frontend/app/modules/dialog/dialog.module.js b/modules/frontend/app/modules/dialog/dialog.module.js
new file mode 100644
index 0000000..6d9324f
--- /dev/null
+++ b/modules/frontend/app/modules/dialog/dialog.module.js
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import igniteDialog from './dialog.directive';
+import igniteDialogTitle from './dialog-title.directive';
+import igniteDialogContent from './dialog-content.directive';
+import IgniteDialog from './dialog.factory';
+
+angular
+.module('ignite-console.dialog', [
+
+])
+.factory('IgniteDialog', IgniteDialog)
+.directive('igniteDialog', igniteDialog)
+.directive('igniteDialogTitle', igniteDialogTitle)
+.directive('igniteDialogContent', igniteDialogContent);
diff --git a/modules/frontend/app/modules/dialog/dialog.tpl.pug b/modules/frontend/app/modules/dialog/dialog.tpl.pug
new file mode 100644
index 0000000..0043709
--- /dev/null
+++ b/modules/frontend/app/modules/dialog/dialog.tpl.pug
@@ -0,0 +1,26 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.modal(tabindex='-1' role='dialog')
+    .modal-dialog
+        .modal-content
+            .modal-header
+                button.close(ng-click='$hide()' aria-hidden='true') &times;
+                h4.modal-title {{title}}
+            .modal-body(ng-show='content')
+                p(ng-bind-html='content' style='text-align: left;')
+            .modal-footer
+                button.btn.btn-primary(id='confirm-btn-confirm' ng-click='$hide()') Ok
diff --git a/modules/frontend/app/modules/form/field/bs-select-placeholder.directive.js b/modules/frontend/app/modules/form/field/bs-select-placeholder.directive.js
new file mode 100644
index 0000000..9d7f59f
--- /dev/null
+++ b/modules/frontend/app/modules/form/field/bs-select-placeholder.directive.js
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Override AngularStrap "bsSelect" in order to dynamically change placeholder and class.
+export default () => {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} $element
+     * @param {ng.IAttributes} attrs
+     * @param {[ng.INgModelController]} [ngModel]
+     */
+    const link = (scope, $element, attrs, [ngModel]) => {
+        if (!ngModel)
+            return;
+
+        const $render = ngModel.$render;
+
+        ngModel.$render = () => {
+            if (scope.$destroyed)
+                return;
+
+            scope.$applyAsync(() => {
+                $render();
+                const value = ngModel.$viewValue;
+
+                if (_.isNil(value) || (attrs.multiple && !value.length)) {
+                    $element.html(attrs.placeholder);
+
+                    $element.addClass('placeholder');
+                }
+                else
+                    $element.removeClass('placeholder');
+            });
+        };
+    };
+
+    return {
+        priority: 1,
+        restrict: 'A',
+        link,
+        require: ['?ngModel']
+    };
+};
diff --git a/modules/frontend/app/modules/form/field/input/autofocus.directive.js b/modules/frontend/app/modules/form/field/input/autofocus.directive.js
new file mode 100644
index 0000000..f54eaee
--- /dev/null
+++ b/modules/frontend/app/modules/form/field/input/autofocus.directive.js
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+/**
+ * @param {ng.ITimeoutService} $timeout
+ */
+export default function factory($timeout) {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} el
+     * @param {ng.IAttributes} attrs
+     */
+    const link = (scope, el, attrs) => {
+        if (_.isUndefined(attrs.igniteFormFieldInputAutofocus) || attrs.igniteFormFieldInputAutofocus !== 'true')
+            return;
+
+        $timeout(() => el.focus(), 100);
+    };
+
+    return {
+        restrict: 'A',
+        link
+    };
+}
+
+factory.$inject = ['$timeout'];
diff --git a/modules/frontend/app/modules/form/form.module.js b/modules/frontend/app/modules/form/form.module.js
new file mode 100644
index 0000000..b17ee87
--- /dev/null
+++ b/modules/frontend/app/modules/form/form.module.js
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+// Field.
+import placeholder from './field/bs-select-placeholder.directive';
+// Validators.
+import ipaddress from './validator/ipaddress.directive';
+import javaKeywords from './validator/java-keywords.directive';
+import javaPackageSpecified from './validator/java-package-specified.directive';
+import javaBuiltInClass from './validator/java-built-in-class.directive';
+import javaIdentifier from './validator/java-identifier.directive';
+import javaPackageName from './validator/java-package-name.directive';
+import propertyValueSpecified from './validator/property-value-specified.directive';
+import propertyUnique from './validator/property-unique.directive';
+import unique from './validator/unique.directive';
+import uuid from './validator/uuid.directive';
+// Helpers.
+import igniteFormFieldInputAutofocus from './field/input/autofocus.directive';
+import IgniteFormGUID from './services/FormGUID.service.js';
+
+angular
+.module('ignite-console.Form', [
+
+])
+// Field.
+.directive('bsSelect', placeholder)
+// Validators.
+.directive('ipaddress', ipaddress)
+.directive('javaKeywords', javaKeywords)
+.directive('javaPackageSpecified', javaPackageSpecified)
+.directive('javaBuiltInClass', javaBuiltInClass)
+.directive('javaIdentifier', javaIdentifier)
+.directive('javaPackageName', javaPackageName)
+.directive('ignitePropertyValueSpecified', propertyValueSpecified)
+.directive('ignitePropertyUnique', propertyUnique)
+.directive('igniteUnique', unique)
+.directive('uuid', uuid)
+// Helpers.
+.directive('igniteFormFieldInputAutofocus', igniteFormFieldInputAutofocus)
+
+// Generator of globally unique identifier.
+.service('IgniteFormGUID', IgniteFormGUID);
diff --git a/modules/frontend/app/modules/form/services/FormGUID.service.js b/modules/frontend/app/modules/form/services/FormGUID.service.js
new file mode 100644
index 0000000..6b8df3c
--- /dev/null
+++ b/modules/frontend/app/modules/form/services/FormGUID.service.js
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default function() {
+    let guid = 0;
+
+    return () => `form-field-${guid++}`;
+}
diff --git a/modules/frontend/app/modules/form/validator/ipaddress.directive.js b/modules/frontend/app/modules/form/validator/ipaddress.directive.js
new file mode 100644
index 0000000..531e682
--- /dev/null
+++ b/modules/frontend/app/modules/form/validator/ipaddress.directive.js
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+/**
+ * @param {ReturnType<typeof import('app/services/InetAddress.service').default>} InetAddress
+ */
+export default function factory(InetAddress) {
+    const onlyDigits = (str) => (/^\d+$/.test(str));
+
+    const strictParseInt = (str) => onlyDigits(str) ? parseInt(str, 10) : Number.NaN;
+
+    const parse = (commonIpAddress) => {
+        const [ipOrHost, portRange] = commonIpAddress.split(':');
+        const ports = _.isUndefined(portRange) ? [] : portRange.split('..').map(strictParseInt);
+
+        return {ipOrHost, ports};
+    };
+
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} el
+     * @param {ng.IAttributes} attrs
+     * @param {[ng.INgModelController]} [ngModel]
+     */
+    const link = (scope, el, attrs, [ngModel]) => {
+        const isEmpty = (modelValue) => {
+            return ngModel.$isEmpty(modelValue) || _.isUndefined(attrs.ipaddress) || attrs.ipaddress !== 'true';
+        };
+
+        const portRange = !_.isNil(attrs.ipaddressWithPortRange);
+
+        if (attrs.ipaddressWithPort) {
+            ngModel.$validators.ipaddressPort = (modelValue) => {
+                if (isEmpty(modelValue) || modelValue.indexOf(':') === -1)
+                    return true;
+
+                if ((modelValue.match(/:/g) || []).length > 1)
+                    return false;
+
+                const {ports} = parse(modelValue);
+
+                if (ports.length !== 1)
+                    return portRange;
+
+                return InetAddress.validPort(ports[0]);
+            };
+        }
+
+        if (portRange) {
+            ngModel.$validators.ipaddressPortRange = (modelValue) => {
+                if (isEmpty(modelValue) || modelValue.indexOf('..') === -1)
+                    return true;
+
+                const {ports} = parse(modelValue);
+
+                if (ports.length !== 2)
+                    return false;
+
+                return InetAddress.validPort(ports[0]) && InetAddress.validPort(ports[1]) && ports[0] < ports[1];
+            };
+        }
+
+        ngModel.$validators.ipaddress = (modelValue) => {
+            if (isEmpty(modelValue))
+                return true;
+
+            const {ipOrHost, ports} = parse(modelValue);
+
+            if (attrs.ipaddressWithPort || attrs.ipaddressWithPortRange || ports.length === 0)
+                return InetAddress.validHost(ipOrHost);
+
+            return false;
+        };
+    };
+
+    return {
+        restrict: 'A',
+        link,
+        require: ['ngModel']
+    };
+}
+
+factory.$inject = ['IgniteInetAddress'];
diff --git a/modules/frontend/app/modules/form/validator/java-built-in-class.directive.js b/modules/frontend/app/modules/form/validator/java-built-in-class.directive.js
new file mode 100644
index 0000000..736179c
--- /dev/null
+++ b/modules/frontend/app/modules/form/validator/java-built-in-class.directive.js
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+/**
+ * @param {import('app/services/JavaTypes.service').default} JavaTypes
+ */
+export default function factory(JavaTypes) {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} el
+     * @param {ng.IAttributes} attrs
+     * @param {[ng.INgModelController]} [ngModel]
+     */
+    const link = (scope, el, attrs, [ngModel]) => {
+        if (_.isUndefined(attrs.javaBuiltInClass) || !attrs.javaBuiltInClass)
+            return;
+
+        ngModel.$validators.javaBuiltInClass = (value) => attrs.validationActive === 'false' ||
+            JavaTypes.nonBuiltInClass(value);
+
+        if (attrs.validationActive !== 'always')
+            attrs.$observe('validationActive', () => ngModel.$validate());
+    };
+
+    return {
+        restrict: 'A',
+        link,
+        require: ['ngModel']
+    };
+}
+
+factory.$inject = ['JavaTypes'];
diff --git a/modules/frontend/app/modules/form/validator/java-identifier.directive.js b/modules/frontend/app/modules/form/validator/java-identifier.directive.js
new file mode 100644
index 0000000..1b5a700
--- /dev/null
+++ b/modules/frontend/app/modules/form/validator/java-identifier.directive.js
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+/**
+ * @param {import('app/services/JavaTypes.service').default} JavaTypes
+ */
+export default function factory(JavaTypes) {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} el
+     * @param {ng.IAttributes} attrs
+     * @param {[ng.INgModelController]} [ngModel]
+     */
+    const link = (scope, el, attrs, [ngModel]) => {
+        if (_.isNil(attrs.javaIdentifier) || attrs.javaIdentifier !== 'true')
+            return;
+
+        /** @type {Array<string>} */
+        const extraValidIdentifiers = scope.$eval(attrs.extraValidJavaIdentifiers) || [];
+
+        ngModel.$validators.javaIdentifier = (value) => attrs.validationActive === 'false' ||
+            _.isEmpty(value) || JavaTypes.validClassName(value) || extraValidIdentifiers.includes(value);
+
+        if (attrs.validationActive !== 'always')
+            attrs.$observe('validationActive', () => ngModel.$validate());
+    };
+
+    return {
+        restrict: 'A',
+        link,
+        require: ['ngModel']
+    };
+}
+
+factory.$inject = ['JavaTypes'];
diff --git a/modules/frontend/app/modules/form/validator/java-keywords.directive.js b/modules/frontend/app/modules/form/validator/java-keywords.directive.js
new file mode 100644
index 0000000..9581ee9
--- /dev/null
+++ b/modules/frontend/app/modules/form/validator/java-keywords.directive.js
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+/**
+ * @param {import('app/services/JavaTypes.service').default} JavaTypes
+ */
+export default function factory(JavaTypes) {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} el
+     * @param {ng.IAttributes} attrs
+     * @param {[ng.INgModelController]} [ngModel]
+     */
+    const link = (scope, el, attrs, [ngModel]) => {
+        if (_.isNil(attrs.javaKeywords) || attrs.javaKeywords === 'false')
+            return;
+
+        const packageOnly = attrs.javaPackageName === 'package-only';
+
+        ngModel.$validators.javaKeywords = (value) => attrs.validationActive === 'false' ||
+            _.isEmpty(value) || !JavaTypes.validClassName(value) ||
+            (!packageOnly && !JavaTypes.packageSpecified(value)) ||
+            _.findIndex(value.split('.'), JavaTypes.isKeyword) < 0;
+
+        if (attrs.validationActive !== 'always')
+            attrs.$observe('validationActive', () => ngModel.$validate());
+    };
+
+    return {
+        restrict: 'A',
+        link,
+        require: ['ngModel']
+    };
+}
+
+factory.$inject = ['JavaTypes'];
diff --git a/modules/frontend/app/modules/form/validator/java-package-name.directive.js b/modules/frontend/app/modules/form/validator/java-package-name.directive.js
new file mode 100644
index 0000000..9059112
--- /dev/null
+++ b/modules/frontend/app/modules/form/validator/java-package-name.directive.js
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+/**
+ * @param {import('app/services/JavaTypes.service').default} JavaTypes
+ */
+export default function factory(JavaTypes) {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} el
+     * @param {ng.IAttributes} attrs
+     * @param {[ng.INgModelController]} [ngModel]
+     */
+    const link = (scope, el, attrs, [ngModel]) => {
+        if (_.isNil(attrs.javaPackageName) || attrs.javaPackageName === 'false')
+            return;
+
+        ngModel.$validators.javaPackageName = (value) => _.isEmpty(value) || JavaTypes.validPackage(value);
+    };
+
+    return {
+        restrict: 'A',
+        link,
+        require: ['ngModel']
+    };
+}
+
+factory.$inject = ['JavaTypes'];
diff --git a/modules/frontend/app/modules/form/validator/java-package-specified.directive.js b/modules/frontend/app/modules/form/validator/java-package-specified.directive.js
new file mode 100644
index 0000000..5ff25d5
--- /dev/null
+++ b/modules/frontend/app/modules/form/validator/java-package-specified.directive.js
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+/**
+ * @param {import('app/services/JavaTypes.service').default} JavaTypes
+ */
+export default function factory(JavaTypes) {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} el
+     * @param {ng.IAttributes} attrs
+     * @param {[ng.INgModelController]} [ngModel]
+     */
+    const link = (scope, el, attrs, [ngModel]) => {
+        if (_.isNil(attrs.javaPackageSpecified) || attrs.javaPackageSpecified === 'false')
+            return;
+
+        const allowBuiltIn = attrs.javaPackageSpecified === 'allow-built-in';
+
+        ngModel.$validators.javaPackageSpecified = (value) => attrs.validationActive === 'false' ||
+            _.isEmpty(value) ||
+            !JavaTypes.validClassName(value) || JavaTypes.packageSpecified(value) ||
+            (allowBuiltIn && !JavaTypes.nonBuiltInClass(value));
+
+        if (attrs.validationActive !== 'always')
+            attrs.$observe('validationActive', () => ngModel.$validate());
+    };
+
+    return {
+        restrict: 'A',
+        link,
+        require: ['ngModel']
+    };
+}
+
+factory.$inject = ['JavaTypes'];
diff --git a/modules/frontend/app/modules/form/validator/property-unique.directive.js b/modules/frontend/app/modules/form/validator/property-unique.directive.js
new file mode 100644
index 0000000..fbcf734
--- /dev/null
+++ b/modules/frontend/app/modules/form/validator/property-unique.directive.js
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+/**
+ * @param {ng.IParseService} $parse
+ */
+export default function factory($parse) {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} el
+     * @param {ng.IAttributes} attrs
+     * @param {[ng.INgModelController]} [ngModel]
+     */
+    const link = (scope, el, attrs, [ngModel]) => {
+        if (_.isUndefined(attrs.ignitePropertyUnique) || !attrs.ignitePropertyUnique)
+            return;
+
+        ngModel.$validators.ignitePropertyUnique = (value) => {
+            const arr = $parse(attrs.ignitePropertyUnique)(scope);
+
+            // Return true in case if array not exist, array empty.
+            if (!value || !arr || !arr.length)
+                return true;
+
+            const key = value.split('=')[0];
+            const idx = _.findIndex(arr, (item) => item.split('=')[0] === key);
+
+            // In case of new element check all items.
+            if (attrs.name === 'new')
+                return idx < 0;
+
+            // Check for $index in case of editing in-place.
+            return (_.isNumber(scope.$index) && (idx < 0 || scope.$index === idx));
+        };
+    };
+
+    return {
+        restrict: 'A',
+        link,
+        require: ['ngModel']
+    };
+}
+
+factory.$inject = ['$parse'];
diff --git a/modules/frontend/app/modules/form/validator/property-value-specified.directive.js b/modules/frontend/app/modules/form/validator/property-value-specified.directive.js
new file mode 100644
index 0000000..5136d1a
--- /dev/null
+++ b/modules/frontend/app/modules/form/validator/property-value-specified.directive.js
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+export default () => {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} el
+     * @param {ng.IAttributes} attrs
+     * @param {[ng.INgModelController]} [ngModel]
+     */
+    const link = (scope, el, attrs, [ngModel]) => {
+        if (_.isUndefined(attrs.ignitePropertyValueSpecified) || !attrs.ignitePropertyValueSpecified)
+            return;
+
+        ngModel.$validators.ignitePropertyValueSpecified = (value) => value ? value.indexOf('=') > 0 : true;
+    };
+
+    return {
+        restrict: 'A',
+        link,
+        require: ['ngModel']
+    };
+};
diff --git a/modules/frontend/app/modules/form/validator/unique.directive.js b/modules/frontend/app/modules/form/validator/unique.directive.js
new file mode 100644
index 0000000..ac6787f
--- /dev/null
+++ b/modules/frontend/app/modules/form/validator/unique.directive.js
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {ListEditableTransclude} from 'app/components/list-editable/components/list-editable-transclude/directive';
+import isNumber from 'lodash/fp/isNumber';
+import get from 'lodash/fp/get';
+
+class Controller {
+    /** @type {ng.INgModelController} */
+    ngModel;
+    /** @type {ListEditableTransclude} */
+    listEditableTransclude;
+    /** @type {Array} */
+    items;
+    /** @type {string?} */
+    key;
+    /** @type {Array<string>} */
+    skip;
+
+    static $inject = ['$scope'];
+
+    /**
+     * @param {ng.IScope} $scope
+     */
+    constructor($scope) {
+        this.$scope = $scope;
+    }
+
+    $onInit() {
+        const isNew = this.key && this.key.startsWith('new');
+        const shouldNotSkip = (item) => get(this.skip[0], item) !== get(...this.skip);
+
+        this.ngModel.$validators.igniteUnique = (value) => {
+            const matches = (item) => (this.key ? item[this.key] : item) === value;
+
+            if (!this.skip) {
+                // Return true in case if array not exist, array empty.
+                if (!this.items || !this.items.length)
+                    return true;
+
+                const idx = this.items.findIndex(matches);
+
+                // In case of new element check all items.
+                if (isNew)
+                    return idx < 0;
+
+                // Case for new component list editable.
+                const $index = this.listEditableTransclude
+                    ? this.listEditableTransclude.$index
+                    : isNumber(this.$scope.$index) ? this.$scope.$index : void 0;
+
+                // Check for $index in case of editing in-place.
+                return (isNumber($index) && (idx < 0 || $index === idx));
+            }
+            // TODO: converge both branches, use $index as idKey
+            return !(this.items || []).filter(shouldNotSkip).some(matches);
+        };
+    }
+
+    $onChanges(changes) {
+        this.ngModel.$validate();
+    }
+}
+
+export default () => {
+    return {
+        controller: Controller,
+        require: {
+            ngModel: 'ngModel',
+            listEditableTransclude: '?^listEditableTransclude'
+        },
+        bindToController: {
+            items: '<igniteUnique',
+            key: '@?igniteUniqueProperty',
+            skip: '<?igniteUniqueSkip'
+        }
+    };
+};
diff --git a/modules/frontend/app/modules/form/validator/uuid.directive.js b/modules/frontend/app/modules/form/validator/uuid.directive.js
new file mode 100644
index 0000000..cb20981
--- /dev/null
+++ b/modules/frontend/app/modules/form/validator/uuid.directive.js
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+/**
+ * @param {import('app/services/JavaTypes.service').default} JavaTypes
+ */
+export default function factory(JavaTypes) {
+    /**
+     * @param {ng.IScope} scope
+     * @param {JQLite} el
+     * @param {ng.IAttributes} attrs
+     * @param {[ng.INgModelController]} [ngModel]
+     */
+    const link = (scope, el, attrs, [ngModel]) => {
+        if (_.isNil(attrs.uuid) || attrs.uuid !== 'true')
+            return;
+
+        ngModel.$validators.uuid = (modelValue) => _.isEmpty(modelValue) || JavaTypes.validUUID(modelValue);
+    };
+
+    return {
+        restrict: 'A',
+        link,
+        require: ['ngModel']
+    };
+}
+
+factory.$inject = ['JavaTypes'];
diff --git a/modules/frontend/app/modules/getting-started/GettingStarted.provider.js b/modules/frontend/app/modules/getting-started/GettingStarted.provider.js
new file mode 100644
index 0000000..b8142aa
--- /dev/null
+++ b/modules/frontend/app/modules/getting-started/GettingStarted.provider.js
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import PAGES from 'app/data/getting-started.json';
+import templateUrl from 'views/templates/getting-started.tpl.pug';
+
+/**
+ * @typedef GettingStartedItem
+ * @prop {string} title
+ * @prop {Array<string>} message
+ */
+
+/**
+ * @typedef {Array<GettingStartedItem>} GettingStartedItems
+ */
+
+export function provider() {
+    /**
+     * Getting started pages.
+     * @type {GettingStartedItems}
+     */
+    const items = PAGES;
+
+    this.push = (before, data) => {
+        const idx = _.findIndex(items, {title: before});
+
+        if (idx < 0)
+            items.push(data);
+        else
+            items.splice(idx, 0, data);
+    };
+
+    this.update = (before, data) => {
+        const idx = _.findIndex(items, {title: before});
+
+        if (idx >= 0)
+            items[idx] = data;
+    };
+
+    this.$get = function() {
+        return items;
+    };
+
+    return this;
+}
+
+/**
+ * @param {ng.IRootScopeService} $root
+ * @param {mgcrea.ngStrap.modal.IModalService} $modal
+ * @param {GettingStartedItems} igniteGettingStarted
+ */
+export function service($root, $modal, igniteGettingStarted) {
+    const _model = igniteGettingStarted;
+
+    let _page = 0;
+
+    const scope = $root.$new();
+
+    scope.ui = {
+        dontShowGettingStarted: false
+    };
+
+    function _fillPage() {
+        if (_page === 0)
+            scope.title = `${_model[_page].title}`;
+        else
+            scope.title = `${_page}. ${_model[_page].title}`;
+
+        scope.message = _model[_page].message.join(' ');
+    }
+
+    scope.isFirst = () => _page === 0;
+
+    scope.isLast = () => _page === _model.length - 1;
+
+    scope.next = () => {
+        _page += 1;
+
+        _fillPage();
+    };
+
+    scope.prev = () => {
+        _page -= 1;
+
+        _fillPage();
+    };
+
+    const dialog = $modal({ templateUrl, scope, show: false, backdrop: 'static'});
+
+    scope.close = () => {
+        try {
+            localStorage.showGettingStarted = !scope.ui.dontShowGettingStarted;
+        }
+        catch (ignore) {
+            // No-op.
+        }
+
+        dialog.hide();
+    };
+
+    return {
+        /**
+         * @param {boolean} force
+         */
+        tryShow: (force) => {
+            try {
+                scope.ui.dontShowGettingStarted = !(_.isNil(localStorage.showGettingStarted)
+                        || localStorage.showGettingStarted === 'true');
+            }
+            catch (ignore) {
+                // No-op.
+            }
+
+            if (force || !scope.ui.dontShowGettingStarted) {
+                _page = 0;
+
+                _fillPage();
+
+                dialog.$promise.then(dialog.show);
+            }
+        }
+    };
+}
+
+service.$inject = ['$rootScope', '$modal', 'igniteGettingStarted'];
+
+export default angular
+    .module('ignite-console.getting-started', [])
+    .provider('igniteGettingStarted', provider)
+    .service('gettingStarted', service);
diff --git a/modules/frontend/app/modules/loading/loading.directive.js b/modules/frontend/app/modules/loading/loading.directive.js
new file mode 100644
index 0000000..983e597
--- /dev/null
+++ b/modules/frontend/app/modules/loading/loading.directive.js
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './loading.pug';
+import './loading.scss';
+
+/**
+ * @param {ReturnType<typeof import('./loading.service').default>} Loading
+ * @param {ng.ICompileService} $compile
+ */
+export default function factory(Loading, $compile) {
+    const link = (scope, element) => {
+        const compiledTemplate = $compile(template);
+
+        const build = () => {
+            scope.position = scope.position || 'middle';
+
+            const loading = compiledTemplate(scope);
+
+            if (!scope.loading) {
+                scope.loading = loading;
+
+                Loading.add(scope.key || 'defaultSpinnerKey', scope.loading);
+                element.append(scope.loading);
+            }
+        };
+
+        build();
+    };
+
+    return {
+        scope: {
+            key: '@igniteLoading',
+            text: '@?igniteLoadingText',
+            class: '@?igniteLoadingClass',
+            position: '@?igniteLoadingPosition'
+        },
+        restrict: 'A',
+        link
+    };
+}
+
+factory.$inject = ['IgniteLoading', '$compile'];
diff --git a/modules/frontend/app/modules/loading/loading.module.js b/modules/frontend/app/modules/loading/loading.module.js
new file mode 100644
index 0000000..ac9e127
--- /dev/null
+++ b/modules/frontend/app/modules/loading/loading.module.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import IgniteLoadingDirective from './loading.directive';
+import IgniteLoadingService from './loading.service';
+
+angular
+    .module('ignite-console.loading', [])
+    .directive('igniteLoading', IgniteLoadingDirective)
+    .service('IgniteLoading', IgniteLoadingService);
diff --git a/modules/frontend/app/modules/loading/loading.pug b/modules/frontend/app/modules/loading/loading.pug
new file mode 100644
index 0000000..cc6cf45
--- /dev/null
+++ b/modules/frontend/app/modules/loading/loading.pug
@@ -0,0 +1,23 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.loading.loading-opacity-80(ng-class='[class, "loading-" + position]')
+    .loading-wrapper
+        .spinner
+            .bounce1
+            .bounce2
+            .bounce3
+        .loading-text {{ text }}
diff --git a/modules/frontend/app/modules/loading/loading.scss b/modules/frontend/app/modules/loading/loading.scss
new file mode 100644
index 0000000..0584c25
--- /dev/null
+++ b/modules/frontend/app/modules/loading/loading.scss
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+[ignite-loading] {
+    position: relative;
+}
+
+.loading {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    top: 0;
+    z-index: 1001;
+    opacity: 0;
+    visibility: hidden;
+    background-color: white;
+    transition: opacity 0.5s linear;
+}
+
+.loading-active {
+    opacity: 1;
+    visibility: visible;
+    transition: opacity 0.5s linear;
+}
+
+.loading:before {
+    content: '';
+    display: inline-block;
+    height: 100%;
+    vertical-align: middle;
+}
+
+.loading .loading-wrapper {
+    display: inline-block;
+    vertical-align: middle;
+    position: relative;
+    width: 100%;
+}
+
+.loading.loading-top .loading-wrapper {
+    position: absolute;
+    top: 100px;
+    display: block;
+}
+
+.loading .loading-text {
+    font-size: 18px;
+    margin: 20px 0;
+    text-align: center;
+}
+
+.loading-opacity-80 {
+    opacity: 0.8;
+}
+
+.loading-max-foreground {
+    z-index: 99999;
+}
diff --git a/modules/frontend/app/modules/loading/loading.service.js b/modules/frontend/app/modules/loading/loading.service.js
new file mode 100644
index 0000000..0aa02a3
--- /dev/null
+++ b/modules/frontend/app/modules/loading/loading.service.js
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default function() {
+    const _overlays = {};
+
+    /**
+     * @param {string} key
+     */
+    const start = (key) => {
+        setTimeout(() => {
+            const loadingOverlay = _overlays[key];
+
+            loadingOverlay && loadingOverlay.addClass('loading-active');
+        });
+    };
+
+    /**
+     * @param {string} key
+     */
+    const finish = (key) => {
+        setTimeout(() => {
+            const loadingOverlay = _overlays[key];
+
+            loadingOverlay && loadingOverlay.removeClass('loading-active');
+        });
+    };
+
+    const add = (key, element) => {
+        _overlays[key] = element;
+
+        return element;
+    };
+
+    return {
+        add,
+        start,
+        finish
+    };
+}
diff --git a/modules/frontend/app/modules/navbar/Userbar.provider.js b/modules/frontend/app/modules/navbar/Userbar.provider.js
new file mode 100644
index 0000000..9b905d7
--- /dev/null
+++ b/modules/frontend/app/modules/navbar/Userbar.provider.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default function() {
+    const items = [];
+
+    this.push = function(data) {
+        items.push(data);
+    };
+
+    this.$get = function() {
+        return items;
+    };
+
+    return this;
+}
diff --git a/modules/frontend/app/modules/navbar/navbar.module.js b/modules/frontend/app/modules/navbar/navbar.module.js
new file mode 100644
index 0000000..e2a4f83
--- /dev/null
+++ b/modules/frontend/app/modules/navbar/navbar.module.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import IgniteUserbar from './Userbar.provider';
+
+angular
+.module('ignite-console.navbar', [
+
+])
+.provider('IgniteUserbar', IgniteUserbar);
diff --git a/modules/frontend/app/modules/nodes/Nodes.service.js b/modules/frontend/app/modules/nodes/Nodes.service.js
new file mode 100644
index 0000000..9130fc6
--- /dev/null
+++ b/modules/frontend/app/modules/nodes/Nodes.service.js
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import nodesDialogTplUrl from './nodes-dialog.tpl.pug';
+
+const DEFAULT_OPTIONS = {
+    grid: {
+        multiSelect: false
+    }
+};
+
+class Nodes {
+    static $inject = ['$q', '$modal'];
+
+    /**
+     * @param {ng.IQService} $q
+     * @param {mgcrea.ngStrap.modal.IModalService} $modal
+     */
+    constructor($q, $modal) {
+        this.$q = $q;
+        this.$modal = $modal;
+    }
+
+    selectNode(nodes, cacheName, options = DEFAULT_OPTIONS) {
+        const { $q, $modal } = this;
+        const defer = $q.defer();
+        options.target = cacheName;
+
+        const modalInstance = $modal({
+            templateUrl: nodesDialogTplUrl,
+            show: true,
+            resolve: {
+                nodes: () => nodes || [],
+                options: () => options
+            },
+            controller: 'nodesDialogController',
+            controllerAs: '$ctrl'
+        });
+
+        modalInstance.$scope.$ok = (data) => {
+            defer.resolve(data);
+            modalInstance.$scope.$hide();
+        };
+
+        modalInstance.$scope.$cancel = () => {
+            defer.reject();
+            modalInstance.$scope.$hide();
+        };
+
+        return defer.promise;
+    }
+}
+
+export default Nodes;
diff --git a/modules/frontend/app/modules/nodes/nodes-dialog.controller.js b/modules/frontend/app/modules/nodes/nodes-dialog.controller.js
new file mode 100644
index 0000000..a0cf80a
--- /dev/null
+++ b/modules/frontend/app/modules/nodes/nodes-dialog.controller.js
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const NID_TEMPLATE = '<div class="ui-grid-cell-contents" title="{{ COL_FIELD }}">{{ COL_FIELD | limitTo:8 }}</div>';
+
+const COLUMNS_DEFS = [
+    {displayName: 'Node ID8', field: 'nid', headerTooltip: 'Node ID8', cellTemplate: NID_TEMPLATE, minWidth: 85, width: 145, pinnedLeft: true},
+    {displayName: 'Node IP', field: 'ip', headerTooltip: 'Primary IP address of node', minWidth: 100, width: 150},
+    {displayName: 'Grid name', field: 'gridName', headerTooltip: 'Name of node grid cluster', minWidth: 110, width: 150},
+    {displayName: 'Version', field: 'version', headerTooltip: 'Node version', minWidth: 75, width: 140},
+    {displayName: 'OS information', field: 'os', headerTooltip: 'OS information for node\'s host', minWidth: 125}
+];
+
+export default function controller($scope, $animate, uiGridConstants, nodes, options) {
+    const $ctrl = this;
+
+    const updateSelected = () => {
+        const nids = $ctrl.gridApi.selection.legacyGetSelectedRows().map((node) => node.nid).sort();
+
+        if (!_.isEqual(nids, $ctrl.selected))
+            $ctrl.selected = nids;
+    };
+
+    $ctrl.nodes = nodes;
+    $ctrl.options = options;
+    $ctrl.selected = [];
+
+    $ctrl.gridOptions = {
+        data: nodes,
+        columnVirtualizationThreshold: 30,
+        columnDefs: COLUMNS_DEFS,
+        enableRowSelection: true,
+        enableRowHeaderSelection: false,
+        enableColumnMenus: false,
+        multiSelect: true,
+        modifierKeysToMultiSelect: true,
+        noUnselect: false,
+        flatEntityAccess: true,
+        fastWatch: true,
+        onRegisterApi: (api) => {
+            $animate.enabled(api.grid.element, false);
+
+            $ctrl.gridApi = api;
+
+            api.selection.on.rowSelectionChanged($scope, updateSelected);
+            api.selection.on.rowSelectionChangedBatch($scope, updateSelected);
+
+            $ctrl.gridApi.grid.element.css('height', '270px');
+
+            setTimeout(() => $ctrl.gridApi.core.notifyDataChange(uiGridConstants.dataChange.COLUMN), 300);
+        },
+        ...options.grid
+    };
+}
+
+controller.$inject = ['$scope', '$animate', 'uiGridConstants', 'nodes', 'options'];
diff --git a/modules/frontend/app/modules/nodes/nodes-dialog.scss b/modules/frontend/app/modules/nodes/nodes-dialog.scss
new file mode 100644
index 0000000..cbb9000
--- /dev/null
+++ b/modules/frontend/app/modules/nodes/nodes-dialog.scss
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.ignite-nodes-dialog {
+    .ui-grid-pinned-container.ui-grid-pinned-container-left .ui-grid-cell:last-child,
+    .ui-grid-pinned-container.ui-grid-pinned-container-left .ui-grid-header-cell:last-child,
+    .ui-grid-header-cell:last-child .ui-grid-column-resizer.right {
+      border-right: none;
+    }
+
+    .modal-dialog {
+        width: 900px;
+    }
+
+    button.pull-left.btn-ignite {
+        background: none;
+    }
+}
diff --git a/modules/frontend/app/modules/nodes/nodes-dialog.tpl.pug b/modules/frontend/app/modules/nodes/nodes-dialog.tpl.pug
new file mode 100644
index 0000000..7bd8c7a
--- /dev/null
+++ b/modules/frontend/app/modules/nodes/nodes-dialog.tpl.pug
@@ -0,0 +1,42 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.modal.modal--ignite.ignite-nodes-dialog(tabindex='-1' role='dialog')
+    .modal-dialog.modal-dialog--adjust-height
+        form.modal-content
+            .modal-header
+                h4.modal-title Select Node
+                button.close(type='button' aria-label='Close' ng-click='$cancel()')
+                     svg(ignite-icon="cross")
+            .modal-body.modal-body-with-scroll
+                p Choose node to execute query for cache: #[strong {{ $ctrl.options.target }}]
+
+                ul.tabs.tabs--blue
+                    li.active(role='presentation')
+                        a
+                            span Cache Nodes
+                            span.badge.badge--blue {{ $ctrl.data.length }}
+
+                .panel--ignite.panel--ignite__without-border
+                    .grid.ui-grid--ignite(ui-grid='$ctrl.gridOptions' ui-grid-resize-columns ui-grid-selection ui-grid-pinning ui-grid-hovering)
+
+            .modal-footer
+                div
+                    grid-item-selected(class='pull-left' grid-api='$ctrl.gridApi')
+
+                div
+                    button.btn-ignite.btn-ignite--link-success(id='confirm-btn-close' ng-click='$cancel()') Cancel
+                    button.btn-ignite.btn-ignite--success(id='confirm-btn-confirm' ng-click='$ok($ctrl.selected)' ng-disabled='$ctrl.selected.length === 0') Select node
diff --git a/modules/frontend/app/modules/nodes/nodes.module.js b/modules/frontend/app/modules/nodes/nodes.module.js
new file mode 100644
index 0000000..4e68b39
--- /dev/null
+++ b/modules/frontend/app/modules/nodes/nodes.module.js
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import './nodes-dialog.scss';
+
+import Nodes from './Nodes.service';
+import nodesDialogController from './nodes-dialog.controller';
+
+angular.module('ignite-console.nodes', [])
+    .service('IgniteNodes', Nodes)
+    .controller('nodesDialogController', nodesDialogController);
diff --git a/modules/frontend/app/modules/socket.module.js b/modules/frontend/app/modules/socket.module.js
new file mode 100644
index 0000000..5a38bdb
--- /dev/null
+++ b/modules/frontend/app/modules/socket.module.js
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import io from 'socket.io-client'; // eslint-disable-line no-unused-vars
+
+angular
+.module('ignite-console.socket', [
+])
+.provider('igniteSocketFactory', function() {
+    let _options = {};
+
+    /**
+     * @param {Object} options Socket io options.
+     */
+    this.set = (options) => {
+        _options = options;
+    };
+
+    function factory(socketFactory) {
+        return function() {
+            const ioSocket = io.connect(_options);
+
+            return socketFactory({ioSocket});
+        };
+    }
+
+    factory.$inject = ['socketFactory'];
+
+    this.$get = factory;
+
+    return this;
+});
diff --git a/modules/frontend/app/modules/states/admin.state.js b/modules/frontend/app/modules/states/admin.state.js
new file mode 100644
index 0000000..68e65cf
--- /dev/null
+++ b/modules/frontend/app/modules/states/admin.state.js
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+angular
+.module('ignite-console.states.admin', [
+    'ui.router'
+])
+.config(['$stateProvider', /** @param {import('@uirouter/angularjs').StateProvider} $stateProvider */ function($stateProvider) {
+    // set up the states
+    $stateProvider
+    .state('base.settings.admin', {
+        url: '/admin',
+        component: 'pageAdmin',
+        permission: 'admin_page',
+        tfMetaTags: {
+            title: 'Admin panel'
+        }
+    });
+}]);
diff --git a/modules/frontend/app/modules/states/errors.state.js b/modules/frontend/app/modules/states/errors.state.js
new file mode 100644
index 0000000..0311b41
--- /dev/null
+++ b/modules/frontend/app/modules/states/errors.state.js
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+angular
+    .module('ignite-console.states.errors', [
+        'ui.router'
+    ])
+    .config(['$stateProvider', /** @param {import('@uirouter/angularjs').StateProvider} $stateProvider */ function($stateProvider) {
+        // set up the states
+        $stateProvider
+            .state('base.404', {
+                url: '/404',
+                component: 'timedRedirection',
+                resolve: {
+                    headerText: () => '404',
+                    subHeaderText: () => 'Page not found'
+                },
+                tfMetaTags: {
+                    title: 'Page not found'
+                },
+                unsaved: true
+            })
+            .state('base.403', {
+                url: '/403',
+                component: 'timedRedirection',
+                resolve: {
+                    headerText: () => '403',
+                    subHeaderText: () => 'You are not authorized'
+                },
+                tfMetaTags: {
+                    title: 'Not authorized'
+                },
+                unsaved: true
+            });
+    }]);
diff --git a/modules/frontend/app/modules/states/logout.state.js b/modules/frontend/app/modules/states/logout.state.js
new file mode 100644
index 0000000..9adc81b
--- /dev/null
+++ b/modules/frontend/app/modules/states/logout.state.js
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+angular.module('ignite-console.states.logout', [
+    'ui.router'
+])
+.config(['$stateProvider', /** @param {import('@uirouter/angularjs').StateProvider} $stateProvider */ function($stateProvider) {
+    // set up the states
+    $stateProvider.state('logout', {
+        url: '/logout',
+        permission: 'logout',
+        controller: ['Auth', function(Auth) {Auth.logout();}],
+        tfMetaTags: {
+            title: 'Logout'
+        }
+    });
+}]);
diff --git a/modules/frontend/app/modules/states/settings.state.js b/modules/frontend/app/modules/states/settings.state.js
new file mode 100644
index 0000000..2c43014
--- /dev/null
+++ b/modules/frontend/app/modules/states/settings.state.js
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+angular
+    .module('ignite-console.states.settings', [
+        'ui.router'
+    ])
+    .config(['$stateProvider', /** @param {import('@uirouter/angularjs').StateProvider} $stateProvider */ function($stateProvider) {
+        // Set up the states.
+        $stateProvider
+            .state('base.settings', {
+                url: '/settings',
+                abstract: true,
+                template: '<ui-view></ui-view>'
+            });
+    }]);
diff --git a/modules/frontend/app/modules/user/Auth.service.ts b/modules/frontend/app/modules/user/Auth.service.ts
new file mode 100644
index 0000000..08cf263
--- /dev/null
+++ b/modules/frontend/app/modules/user/Auth.service.ts
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {StateService} from '@uirouter/angularjs';
+import MessagesFactory from '../../services/Messages.service';
+import {service as GettingsStartedFactory} from '../../modules/getting-started/GettingStarted.provider';
+import UserServiceFactory from './User.service';
+
+type SignupUserInfo = {
+    email: string,
+    password: string,
+    firstName: string,
+    lastName: string,
+    company: string,
+    country: string,
+};
+
+type AuthActions = 'signin' | 'signup' | 'password/forgot';
+type AuthOptions = {email:string, password:string, activationToken?: string}|SignupUserInfo|{email:string};
+
+export default class AuthService {
+    static $inject = ['$http', '$rootScope', '$state', '$window', 'IgniteMessages', 'gettingStarted', 'User'];
+
+    constructor(
+        private $http: ng.IHttpService,
+        private $root: ng.IRootScopeService,
+        private $state: StateService,
+        private $window: ng.IWindowService,
+        private Messages: ReturnType<typeof MessagesFactory>,
+        private gettingStarted: ReturnType<typeof GettingsStartedFactory>,
+        private User: ReturnType<typeof UserServiceFactory>
+    ) {}
+
+    signup(userInfo: SignupUserInfo, loginAfterSignup: boolean = true) {
+        return this._auth('signup', userInfo, loginAfterSignup);
+    }
+
+    signin(email: string, password: string, activationToken?: string) {
+        return this._auth('signin', {email, password, activationToken});
+    }
+
+    remindPassword(email: string) {
+        return this._auth('password/forgot', {email}).then(() => this.$state.go('password.send'));
+    }
+
+    // TODO IGNITE-7994: Remove _auth and move API calls to corresponding methods
+    /**
+     * Performs the REST API call.
+     */
+    private _auth(action: AuthActions, userInfo: AuthOptions, loginAfterwards: boolean = true) {
+        return this.$http.post('/api/v1/' + action, userInfo)
+            .then(() => {
+                if (action === 'password/forgot')
+                    return;
+
+                this.User.read()
+                    .then((user) => {
+                        if (loginAfterwards) {
+                            this.$root.$broadcast('user', user);
+                            this.$state.go('default-state');
+                            this.$root.gettingStarted.tryShow();
+                        } else
+                            this.$root.$broadcast('userCreated');
+                    });
+            });
+    }
+    logout() {
+        return this.$http.post('/api/v1/logout')
+            .then(() => {
+                this.User.clean();
+
+                this.$window.open(this.$state.href('signin'), '_self');
+            })
+            .catch((e) => this.Messages.showError(e));
+    }
+
+    async resendSignupConfirmation(email: string) {
+        try {
+            return await this.$http.post('/api/v1/activation/resend/', {email});
+        } catch (res) {
+            throw res.data;
+        }
+    }
+}
diff --git a/modules/frontend/app/modules/user/User.service.js b/modules/frontend/app/modules/user/User.service.js
new file mode 100644
index 0000000..da84dcc
--- /dev/null
+++ b/modules/frontend/app/modules/user/User.service.js
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {ReplaySubject} from 'rxjs';
+
+/**
+ * @typedef User
+ * @prop {string} _id
+ * @prop {boolean} admin
+ * @prop {string} country
+ * @prop {string} email
+ * @prop {string} firstName
+ * @prop {string} lastName
+ * @prop {string} lastActivity
+ * @prop {string} lastLogin
+ * @prop {string} registered
+ * @prop {string} token
+ */
+
+/**
+ * @param {ng.IQService} $q
+ * @param {ng.auto.IInjectorService} $injector
+ * @param {ng.IRootScopeService} $root
+ * @param {import('@uirouter/angularjs').StateService} $state
+ * @param {ng.IHttpService} $http
+ */
+export default function User($q, $injector, $root, $state, $http) {
+    /** @type {ng.IPromise<User>} */
+    let user;
+
+    const current$ = new ReplaySubject(1);
+
+    return {
+        current$,
+        /**
+         * @returns {ng.IPromise<User>}
+         */
+        load() {
+            return user = $http.post('/api/v1/user')
+                .then(({data}) => {
+                    $root.user = data;
+
+                    $root.$broadcast('user', $root.user);
+
+                    current$.next(data);
+
+                    return $root.user;
+                })
+                .catch(({data}) => {
+                    user = null;
+
+                    return $q.reject(data);
+                });
+        },
+        read() {
+            if (user)
+                return user;
+
+            return this.load();
+        },
+        clean() {
+            delete $root.user;
+
+            delete $root.IgniteDemoMode;
+
+            sessionStorage.removeItem('IgniteDemoMode');
+        }
+    };
+}
+
+User.$inject = ['$q', '$injector', '$rootScope', '$state', '$http'];
diff --git a/modules/frontend/app/modules/user/emailConfirmationInterceptor.ts b/modules/frontend/app/modules/user/emailConfirmationInterceptor.ts
new file mode 100644
index 0000000..ce377b1
--- /dev/null
+++ b/modules/frontend/app/modules/user/emailConfirmationInterceptor.ts
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {UIRouter} from '@uirouter/angularjs';
+
+registerInterceptor.$inject = ['$httpProvider'];
+
+export function registerInterceptor(http: ng.IHttpProvider) {
+    emailConfirmationInterceptor.$inject = ['$q', '$injector'];
+
+    function emailConfirmationInterceptor($q: ng.IQService, $injector: ng.auto.IInjectorService): ng.IHttpInterceptor {
+        return {
+            responseError(res) {
+                if (res.status === 403 && res.data && res.data.errorCode === 10104)
+                    $injector.get<UIRouter>('$uiRouter').stateService.go('signup-confirmation', {email: res.data.email});
+
+                return $q.reject(res);
+            }
+        };
+    }
+
+    http.interceptors.push(emailConfirmationInterceptor as ng.IHttpInterceptorFactory);
+}
diff --git a/modules/frontend/app/modules/user/permissions.js b/modules/frontend/app/modules/user/permissions.js
new file mode 100644
index 0000000..616226a
--- /dev/null
+++ b/modules/frontend/app/modules/user/permissions.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const guest = ['login'];
+const becomed = ['profile', 'configuration'];
+const user = becomed.concat(['logout', 'query', 'demo']);
+const admin = user.concat(['admin_page']);
+
+export default {
+    guest,
+    user,
+    admin,
+    becomed
+};
diff --git a/modules/frontend/app/modules/user/user.module.js b/modules/frontend/app/modules/user/user.module.js
new file mode 100644
index 0000000..370d470
--- /dev/null
+++ b/modules/frontend/app/modules/user/user.module.js
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import aclData from './permissions';
+
+import Auth from './Auth.service';
+import User from './User.service';
+import {registerInterceptor} from './emailConfirmationInterceptor';
+
+/**
+ * @param {ng.auto.IInjectorService} $injector
+ * @param {ng.IQService} $q
+ */
+function sessionRecoverer($injector, $q) {
+    /** @type {ng.IHttpInterceptor} */
+    return {
+        responseError: (response) => {
+            // Session has expired
+            if (response.status === 401) {
+                $injector.get('User').clean();
+
+                const stateName = $injector.get('$uiRouterGlobals').current.name;
+
+                if (!_.includes(['', 'signin', 'terms', '403', '404'], stateName))
+                    $injector.get('$state').go('signin');
+            }
+
+            return $q.reject(response);
+        }
+    };
+}
+
+sessionRecoverer.$inject = ['$injector', '$q'];
+
+/**
+ * @param {ng.IRootScopeService} $root
+ * @param {import('@uirouter/angularjs').TransitionService} $transitions
+ * @param {unknown} AclService
+ * @param {ReturnType<typeof import('./User.service').default>} User
+ * @param {ReturnType<typeof import('app/components/activities-user-dialog/index').default>} Activities
+ */
+function run($root, $transitions, AclService, User, Activities) {
+    AclService.setAbilities(aclData);
+    AclService.attachRole('guest');
+
+    $root.$on('user', (event, user) => {
+        if (!user)
+            return;
+
+        AclService.flushRoles();
+
+        let role = 'user';
+
+        if (user.admin)
+            role = 'admin';
+
+        if (user.becomeUsed)
+            role = 'becomed';
+
+        AclService.attachRole(role);
+    });
+
+    $transitions.onBefore({}, (trans) => {
+        const $state = trans.router.stateService;
+        const {permission} = trans.to();
+
+        if (_.isEmpty(permission))
+            return;
+
+        return trans.injector().get('User').read()
+            .then(() => {
+                if (!AclService.can(permission))
+                    throw new Error('Illegal access error');
+            })
+            .catch(() => {
+                return $state.target(trans.to().failState || 'base.403');
+            });
+    });
+}
+
+run.$inject = ['$rootScope', '$transitions', 'AclService', 'User', 'IgniteActivitiesData'];
+
+angular
+    .module('ignite-console.user', [
+        'mm.acl',
+        'ignite-console.config',
+        'ignite-console.core'
+    ])
+    .factory('sessionRecoverer', sessionRecoverer)
+    .config(registerInterceptor)
+    .config(['$httpProvider', ($httpProvider) => {
+        $httpProvider.interceptors.push('sessionRecoverer');
+    }])
+    .service('Auth', Auth)
+    .service('User', User)
+    .run(run);
diff --git a/modules/frontend/app/primitives/badge/index.scss b/modules/frontend/app/primitives/badge/index.scss
new file mode 100644
index 0000000..59f28cc
--- /dev/null
+++ b/modules/frontend/app/primitives/badge/index.scss
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../../public/stylesheets/variables';
+
+.badge {
+  display: inline-block;
+  min-width: 26px;
+  height: 18px;
+
+  padding: 3px 9px;
+
+  border-radius: 9px;
+
+  color: white;
+  font-size: 12px;
+  font-weight: 500;
+  text-align: center;
+  line-height: 12px;
+
+  background-color: $brand-primary;
+}
+
+.badge--blue {
+  background-color: #0067b9;
+}
\ No newline at end of file
diff --git a/modules/frontend/app/primitives/btn-group/index.pug b/modules/frontend/app/primitives/btn-group/index.pug
new file mode 100644
index 0000000..702da60
--- /dev/null
+++ b/modules/frontend/app/primitives/btn-group/index.pug
@@ -0,0 +1,39 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin btn-group(disabled, options, tip)
+    .btn-group.panel-tip-container&attributes(attributes)
+        button.btn.btn-primary(
+            ng-click=`${options[0].click}`
+
+            data-ng-disabled=disabled && `${disabled}`
+
+            bs-tooltip=''
+            data-title=tip
+
+            data-trigger='hover'
+            data-placement='bottom'
+        ) #{options[0].text}
+        button.btn.dropdown-toggle.btn-primary(
+            data-ng-disabled=disabled && `${disabled}` || `!${JSON.stringify(options)}.length`
+
+            bs-dropdown=`${JSON.stringify(options)}`
+
+            data-toggle='dropdown'
+            data-container='body'
+            data-placement='bottom-right'
+        )
+            span.caret
diff --git a/modules/frontend/app/primitives/btn/index.scss b/modules/frontend/app/primitives/btn/index.scss
new file mode 100644
index 0000000..ba97896
--- /dev/null
+++ b/modules/frontend/app/primitives/btn/index.scss
@@ -0,0 +1,375 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import "./../../../public/stylesheets/variables.scss";
+
+$btn-content-padding: 10px 12px;
+$btn-content-padding-with-border: 9px 11px;
+
+@mixin active-focus-shadows(
+    $focus: (0 0 5px #095d9a, 0 0 5px #095d9a),
+    $active: (inset 0 1px 3px 0 rgba(0, 0, 0, 0.5))
+) {
+    &.focus, &:focus {
+        box-shadow: $focus;
+    }
+
+    &.active, &:active {
+        box-shadow: $active;
+    }
+
+    &:active, &.active {
+        &:focus, &.focus {
+            &:not([disabled]) {
+                box-shadow: $focus, $active;
+            }
+        }
+    }
+}
+
+.btn-ignite {
+    $icon-margin: 8px;
+
+    display: inline-flex;
+    flex-direction: row;
+    justify-content: center;
+    align-items: center;
+    box-sizing: border-box;
+    margin: 0;
+    padding: $btn-content-padding;
+
+    border: none;
+    border-radius: $ignite-button-border-radius;
+    text-align: center;
+    outline: none;
+    font-size: 14px;
+    line-height: 16px;
+    text-decoration: none;
+    cursor: pointer;
+
+    .icon {
+        &, &-right, &-left {
+            height: 16px;
+        }
+
+        &-right {
+            margin-left: $icon-margin;
+        }
+
+        &-left {
+            margin-right: $icon-margin;
+        }
+    }
+
+    // Icon tweaks
+    .icon.fa-caret-down {
+        margin: 0 -3px;
+    }
+
+    .fa {
+        line-height: inherit !important;
+        font-size: 16px;
+    }
+
+    &[disabled] {
+        -webkit-pointer-events: none;
+        pointer-events: none;
+    }
+
+    [ignite-icon='plus'] {
+        height: 12px;
+        width: 12px;
+    }
+}
+
+.btn-ignite--primary {
+    $accent-color: $ignite-brand-primary;
+    $text-color: white;
+
+    background-color: $accent-color;
+    color: white;
+
+    &:hover, &.hover,
+    &:active, &.active {
+        &:not([disabled]) {
+            color: white !important;
+            background-color: change-color($accent-color, $lightness: 41%);
+            text-decoration: none !important;
+        }
+    }
+
+    // Override <a> styles
+    &:focus {
+        color: white !important;
+        text-decoration: none !important;
+    }
+
+    @include active-focus-shadows();
+
+    &[disabled] {
+        color: transparentize($text-color, 0.5);
+        background-color: change-color($accent-color, $lightness: 77%);
+    }
+}
+
+.btn-ignite--primary-outline {
+    $accent-color: $ignite-brand-primary;
+    $hover-color: change-color($accent-color, $lightness: 36%);
+    $disabled-color: #c5c5c5;
+
+    border: 1px solid $accent-color;
+    background: white;
+    color: $accent-color;
+    padding: $btn-content-padding-with-border;
+
+    &:hover, &.hover,
+    &:active, &.active {
+        &:not([disabled]) {
+            color: $hover-color;
+            border-color: $hover-color;
+        }
+    }
+
+    @include active-focus-shadows($active: inset 0 1px 3px 0 $hover-color);
+
+    &[disabled] {
+        color: $disabled-color;
+        border-color: $disabled-color;
+    }
+}
+
+.btn-ignite--success {
+    $accent-color: $ignite-brand-success;
+    $text-color: white;
+
+    background-color: $accent-color;
+    color: white;
+
+    &:hover, &.hover, &:focus,
+    &:active, &.active {
+        &:not([disabled]) {
+            color: white;
+            background-color: change-color($accent-color, $lightness: 26%);
+        }
+    }
+
+    @include active-focus-shadows();
+
+    &[disabled] {
+        color: transparentize($text-color, 0.5);
+        background-color: change-color($accent-color, $saturation: 57%, $lightness: 68%);
+    }
+}
+
+.btn-ignite--link-success {
+    $accent-color: $ignite-brand-success;
+
+    background: transparent;
+    color: $accent-color;
+    text-decoration: underline;
+
+    &:hover, &.hover,
+    &:active, &.active {
+        &:not([disabled]) {
+            color: change-color($accent-color, $lightness: 26%);
+        }
+    }
+
+    &[disabled] {
+        color: change-color($accent-color, $saturation: 57%, $lightness: 68%);
+    }
+
+}
+
+@mixin btn-ignite--link-dashed(
+    $color,
+    $activeHover,
+    $disabled
+) {
+    background: transparent;
+    color: $color;
+
+    span {
+        background:
+            linear-gradient(to right, $color, transparent),
+            linear-gradient(to right, $color 70%, transparent 0%) repeat-x left bottom;
+        background-size: 0, 8px 1px, 0, 0;
+    }
+
+    &:hover, &.hover,
+    &:active, &.active {
+        &:not([disabled]) {
+            color: $activeHover;
+
+            span {
+                background:
+                    linear-gradient(to right, $activeHover, transparent),
+                    linear-gradient(to right, $activeHover 70%, transparent 0%) repeat-x left bottom;
+                background-size: 0, 8px 1px, 0, 0;
+            }
+        }
+    }
+
+    &[disabled] {
+        color: $disabled;
+
+        span {
+            background:
+                linear-gradient(to right, $disabled, transparent),
+                linear-gradient(to right, $disabled 70%, transparent 0%) repeat-x left bottom;
+            background-size: 0, 8px 1px, 0, 0;
+        }
+    }
+
+    @include active-focus-shadows($active: ());
+}
+
+.btn-ignite--link-dashed-success {
+    $color: $ignite-brand-success;
+    $activeHover: change-color($color, $lightness: 26%);
+    $disabled: change-color($color, $saturation: 57%, $lightness: 68%);
+
+    @include btn-ignite--link-dashed($color, $activeHover, $disabled);
+}
+
+.btn-ignite--link-dashed-primary {
+    $color: $ignite-brand-primary;
+    $activeHover: change-color($color, $lightness: 26%);
+    $disabled: change-color($color, $saturation: 57%, $lightness: 68%);
+
+    @include btn-ignite--link-dashed($color, $activeHover, $disabled);
+}
+
+.btn-ignite--link-dashed-primary {
+    $color: $ignite-brand-primary;
+    $activeHover: change-color($color, $lightness: 26%);
+    $disabled: change-color($color, $saturation: 57%, $lightness: 68%);
+
+    @include btn-ignite--link-dashed($color, $activeHover, $disabled);
+}
+
+.btn-ignite--link-dashed-secondary {
+    $activeHover: change-color($ignite-brand-success, $lightness: 26%);
+    @include btn-ignite--link-dashed($text-color, $activeHover, $gray-light);
+}
+
+.btn-ignite--secondary {
+    background-color: white;
+    color: #424242;
+    border: 1px solid #c5c5c5;
+    padding: $btn-content-padding-with-border;
+
+    &:hover, &.hover,
+    &:active, &.active {
+        &:not([disabled]) {
+            border-color: #c5c5c5;
+            background-color: #eeeeee;
+        }
+    }
+
+    @include active-focus-shadows();
+
+    &[disabled] {
+        opacity: 0.5;
+    }
+}
+
+.btn-ignite-group {
+    display: inline-flex;
+
+    .btn-ignite:not(:first-of-type):not(:last-of-type) {
+        border-radius: 0;
+    }
+
+    .btn-ignite:not(:last-of-type) {
+        border-right-width: 1px;
+        border-right-style: solid;
+    }
+
+    .btn-ignite:first-of-type:not(:only-child) {
+        border-top-right-radius: 0;
+        border-bottom-right-radius: 0;
+    }
+
+    .btn-ignite:last-of-type:not(:only-child) {
+        border-top-left-radius: 0;
+        border-bottom-left-radius: 0;
+    }
+
+    .btn-ignite.btn-ignite--primary {
+        $line-color: $ignite-brand-primary;
+        border-right-color: change-color($line-color, $lightness: 41%);
+    }
+    .btn-ignite.btn-ignite--success {
+        $line-color: $ignite-brand-success;
+        border-right-color: change-color($line-color, $saturation: 63%, $lightness: 33%);
+    }
+    .btn-ignite.btn-ignite--secondary + .btn-ignite.btn-ignite--secondary {
+        border-left: 0;
+    }
+
+    &[disabled] .btn-ignite.btn-ignite--primary {
+        border-right-color: change-color($ignite-brand-primary, $lightness: 83%);
+    }
+
+    &[disabled] .btn-ignite.btn-ignite--success {
+        border-right-color: change-color($ignite-brand-success, $lightness: 83%);
+    }
+}
+
+@mixin ignite-link($color, $color-hover) {
+    color: $color;
+    text-decoration: none;
+
+    &:hover, &.hover,
+    &:focus, &.focus {
+        color: $color-hover;
+        text-decoration: none;
+    }
+}
+
+.link-primary {
+    @include ignite-link(
+        $color: $ignite-brand-primary,
+        $color-hover: change-color($ignite-brand-primary, $lightness: 41%)
+    );
+}
+
+.link-success {
+    @include ignite-link(
+        $color: $ignite-brand-success,
+        $color-hover: change-color($ignite-brand-success, $lightness: 26%)
+    );
+}
+
+.btn-ignite--link {
+    background: transparent;
+
+    @include ignite-link(
+        $color: $ignite-brand-success,
+        $color-hover: change-color($ignite-brand-success, $lightness: 26%)
+    );
+}
+
+.btn-ignite--link {
+    background: transparent;
+
+    @include ignite-link(
+        $color: $ignite-brand-success,
+        $color-hover: change-color($ignite-brand-success, $lightness: 26%)
+    );
+}
diff --git a/modules/frontend/app/primitives/checkbox/index.scss b/modules/frontend/app/primitives/checkbox/index.scss
new file mode 100644
index 0000000..847e33c
--- /dev/null
+++ b/modules/frontend/app/primitives/checkbox/index.scss
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.theme--ignite {
+    .form-field-checkbox {
+        z-index: 2;
+        padding-left: 8px;
+        padding-right: 8px;
+
+        input[type='checkbox'] {
+            margin-right: 8px;
+            vertical-align: -1px;
+        }
+        .tipLabel {
+            vertical-align: -3px;
+        }
+    }    
+}
diff --git a/modules/frontend/app/primitives/datepicker/index.pug b/modules/frontend/app/primitives/datepicker/index.pug
new file mode 100644
index 0000000..dd586dc
--- /dev/null
+++ b/modules/frontend/app/primitives/datepicker/index.pug
@@ -0,0 +1,65 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__datepicker({ label, model, name, mindate, maxdate, minview = 1, format = 'MMM yyyy', disabled, required, placeholder, tip })
+    mixin __form-field__datepicker()
+        input(
+            id=`{{ ${name} }}Input`
+            name=`{{ ${name} }}`
+
+            placeholder=placeholder
+            
+            ng-model=model
+
+            ng-required=required && `${required}`
+            ng-disabled=disabled && `${disabled}`
+
+            bs-datepicker
+
+            data-min-date=mindate ? `{{ ${mindate} }}` : false
+            data-max-date=maxdate ? `{{ ${maxdate} }}` : `today`
+
+            data-min-view=minview
+            data-date-format=format
+            data-start-view=minview
+
+            data-placement='bottom'
+            data-container='.ignite-form-field'
+
+            tabindex='0'
+
+            onkeydown='return false',
+            ng-ref='$input'
+            ng-ref-read='ngModel'
+        )&attributes(attributes.attributes)
+
+    .form-field.form-field__datepicker.ignite-form-field(id=`{{ ${name} }}Field`)
+        +form-field__label({ label, name, required, disabled })
+            +form-field__tooltip({ title: tip, options: tipOpts })
+
+        .form-field__control
+            - attributes.type='button'
+            +__form-field__datepicker(attributes=attributes)
+
+        .form-field__errors(
+            ng-messages=`$input.$error`
+            ng-show=`($input.$dirty || $input.$touched || $input.$submitted) && $input.$invalid`
+        )
+            if required
+                +form-field__error({ error: 'required', message: `${errLbl} could not be empty!` })
+
+            if block
+                block
diff --git a/modules/frontend/app/primitives/datepicker/index.scss b/modules/frontend/app/primitives/datepicker/index.scss
new file mode 100644
index 0000000..c527a42
--- /dev/null
+++ b/modules/frontend/app/primitives/datepicker/index.scss
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.datepicker.dropdown-menu {
+    width: 250px;
+    height: 270px;
+    z-index: 2000;
+
+    button {
+        outline: none;
+        border: 0;
+    }
+
+    tbody {
+        height: 180px;
+    }
+
+    tbody button {
+        padding: 6px;
+        height: 100%;
+    }
+
+    &.datepicker-mode-1, &.datepicker-mode-2 {
+        tbody button {
+            height: 65px;
+        }
+    }
+}
diff --git a/modules/frontend/app/primitives/dropdown/index.pug b/modules/frontend/app/primitives/dropdown/index.pug
new file mode 100644
index 0000000..aad6efd
--- /dev/null
+++ b/modules/frontend/app/primitives/dropdown/index.pug
@@ -0,0 +1,42 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin ignite-form-field-bsdropdown({label, model, name, disabled, required, options, tip})
+    .dropdown--ignite.ignite-form-field
+        .btn-ignite.btn-ignite--primary-outline(
+            ng-model=model
+
+            ng-required=required && `${required}`
+            ng-disabled=disabled && `${disabled}` || `!${options}.length`
+
+            bs-dropdown=''
+
+            data-trigger='hover focus'
+            data-placement='bottom-right'
+            data-container='self'
+
+            tabindex='0'
+            aria-haspopup='true'
+            aria-expanded='false'
+        )&attributes(attributes)
+            | !{label}
+            span.icon-right.fa.fa-caret-down
+
+        ul.dropdown-menu(role='menu')
+            li(ng-repeat=`item in ${options}` ng-if='item.available')
+                a(ng-if='item.click' ng-click='item.click()') {{ item.action }}
+                a(ng-if='item.sref' ui-sref='{{:: item.sref}}') {{ item.action }}
+
diff --git a/modules/frontend/app/primitives/dropdown/index.scss b/modules/frontend/app/primitives/dropdown/index.scss
new file mode 100644
index 0000000..4695d21
--- /dev/null
+++ b/modules/frontend/app/primitives/dropdown/index.scss
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../../public/stylesheets/variables';
+
+.ignite-form-field {
+    font-style: normal;
+
+    ul.dropdown-menu {
+        z-index: 1;
+
+        min-width: calc(100% + 2px);
+        margin-top: 2px;
+        padding: 0;
+
+        border-color: #c5c5c5;
+        box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.3);
+
+        li {
+            a {
+                padding-left: 15px;
+                padding-right: 15px;
+
+                color: $text-color;
+                line-height: 26px;
+
+                &:hover {
+                    color: #a8110f;
+                }
+
+                &.active {
+                    background-color: #fff;
+                    font-weight: normal;
+                }
+            }
+        }
+
+        li:not(:last-child) {
+            border-bottom: 1px solid $table-border-color;
+        }
+    }
+}
+
+.dropdown--ignite {
+    .btn-ignite {
+        position: relative;
+
+        &::after {
+            content: '';
+
+            position: absolute;
+            bottom: -4px;
+
+            display: block;
+            height: 4px;
+            width: 100%;
+        }
+
+        &[disabled='disabled'] {
+            ul.dropdown-menu {
+                display: none !important;
+            }
+        }
+    }
+}
diff --git a/modules/frontend/app/primitives/form-field/checkbox.pug b/modules/frontend/app/primitives/form-field/checkbox.pug
new file mode 100644
index 0000000..fe0f808
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/checkbox.pug
@@ -0,0 +1,31 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__checkbox({ label, model, name, disabled, required, tip, tipOpts })
+    .form-field.ignite-form-field.form-field__checkbox(id=`{{ ${name} }}Field`)
+        +form-field__label({ label, name, required, disabled })
+            +form-field__tooltip({ title: tip, options: tipOpts })
+
+        .form-field__control
+            - attributes.type='checkbox'
+            +form-field__input({ name, model, disabled, required, placeholder })(attributes=attributes)
+
+        .form-field__errors(
+            ng-messages=`$input.$error`
+            ng-show=`($input.$dirty || $input.$touched || $input.$submitted) && $input.$invalid`
+        )
+            if block
+                block
diff --git a/modules/frontend/app/primitives/form-field/dropdown.pug b/modules/frontend/app/primitives/form-field/dropdown.pug
new file mode 100644
index 0000000..73cced5
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/dropdown.pug
@@ -0,0 +1,59 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__dropdown({ label, model, name, disabled, required, multiple, placeholder, placeholderEmpty, options, optionLabel = 'label', tip, tipOpts, change })
+    -var errLbl = label ? label.substring(0, label.length - 1) : 'Field';
+
+    mixin __form-field__input()
+        button.select-toggle(
+            type='button'
+            id=`{{ ${name} }}Input`
+            name=`{{ ${name} }}`
+
+            data-placeholder=placeholderEmpty ? `{{ ${options}.length > 0 ? '${placeholder}' : '${placeholderEmpty}' }}` : placeholder
+            
+            ng-model=model
+            ng-disabled=disabled && `${disabled}`
+            ng-required=required && `${required}`
+            ng-ref='$input'
+            ng-ref-read='ngModel'
+
+            ng-change=change && `${change}`
+
+            bs-select
+            bs-options=`item.value as item.${optionLabel} for item in ${options}`
+
+            data-multiple=multiple ? '1' : false
+
+            tabindex='0'
+        )&attributes(attributes.attributes)
+
+    .form-field.form-field__dropdown.ignite-form-field(id=`{{ ${name} }}Field`)
+        +form-field__label({ label, name, required, disabled })
+            +form-field__tooltip({ title: tip, options: tipOpts })
+
+        .form-field__control
+            +__form-field__input(attributes=attributes)
+
+        .form-field__errors(
+            ng-messages=`$input.$error`
+            ng-show=`($input.$dirty || $input.$touched || $input.$submitted) && $input.$invalid`
+        )
+            if required
+                +form-field__error({ error: 'required', message: `${errLbl} could not be empty!` })
+
+            if block
+                block
diff --git a/modules/frontend/app/primitives/form-field/email.pug b/modules/frontend/app/primitives/form-field/email.pug
new file mode 100644
index 0000000..b1e1202
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/email.pug
@@ -0,0 +1,38 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__email({ label, model, name, disabled, required, placeholder, tip })
+    -let errLbl = label[label.length - 1] === ':' ? label.substring(0, label.length - 1) : label
+
+    .form-field.ignite-form-field
+        +form-field__label({ label, name, required, disabled })
+            +form-field__tooltip({ title: tip, options: tipOpts })
+
+        .form-field__control
+            - attributes.type='email'
+            +form-field__input({ name, model, disabled, required, placeholder })(attributes=attributes)
+
+        .form-field__errors(
+            ng-messages=`$input.$error`
+            ng-show=`($input.$dirty || $input.$touched || $input.$submitted) && $input.$invalid`
+        )
+            if required
+                +form-field__error({ error: 'required', message: `${errLbl} could not be empty!` })
+
+            +form-field__error({ error: 'email', message: `${errLbl} has invalid format!` })
+
+            if block
+                block
diff --git a/modules/frontend/app/primitives/form-field/error.pug b/modules/frontend/app/primitives/form-field/error.pug
new file mode 100644
index 0000000..9b44c3c
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/error.pug
@@ -0,0 +1,30 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__error({ error, message })
+    .form-field__error(ng-message=error)
+        div #{ message }
+        div(
+            bs-tooltip=''
+            data-title=message
+            data-placement='top'
+            data-template=`
+                <div class="tooltip tooltip--error in" ng-show="title">
+                    <div class="tooltip-arrow"></div>
+                    <div class="tooltip-inner" ng-bind-html="title"></div>
+                </div>`
+        )
+        svg(ignite-icon='exclamation')
diff --git a/modules/frontend/app/primitives/form-field/index.pug b/modules/frontend/app/primitives/form-field/index.pug
new file mode 100644
index 0000000..5a54f21
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/index.pug
@@ -0,0 +1,29 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include ./error
+include ./label
+include ./tooltip
+include ./input
+include ./text
+include ./number
+include ./email
+include ./password
+include ./phone
+include ./dropdown
+include ./checkbox
+include ./typeahead
+include ./radio
diff --git a/modules/frontend/app/primitives/form-field/index.scss b/modules/frontend/app/primitives/form-field/index.scss
new file mode 100644
index 0000000..0ddc194
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/index.scss
@@ -0,0 +1,706 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../../public/stylesheets/variables';
+
+.theme--ignite {
+    [ignite-icon='info'], .tipLabel {
+        color: $ignite-brand-success;
+    }
+
+    .ignite-form-field {
+        width: 100%;
+
+        &.radio--ignite {
+            width: auto;
+
+        }
+
+        &.ignite-form-field-dropdown {
+            .ignite-form-field__control button {
+                display: inline-block;
+                overflow: hidden !important;
+                text-overflow: ellipsis;
+            }
+        }
+
+        [ignite-icon='info'], .tipLabel {
+            margin-left: 4px;
+            flex: 0 0 auto;
+        }
+
+
+        label.required {
+            content: '*';
+            margin-left: 0.25em;
+        }
+
+        .ignite-form-field__control {
+            width: 100%;
+
+            .input-tip {
+                display: flex;
+                overflow: visible;
+
+                & > input,
+                & > button {
+                    overflow: visible;
+
+                    box-sizing: border-box;
+                    width: 100%;
+                    max-width: initial;
+                    height: 36px;
+                    padding: 0 10px;
+                    margin-right: 0;
+
+                    border: solid 1px #c5c5c5;
+                    border-radius: 4px;
+                    background-color: #ffffff;
+                    box-shadow: inset 0 1px 3px 0 rgba(0, 0, 0, 0.2);
+
+                    color: $text-color;
+                    line-height: 36px;
+
+                    &.ng-invalid:not(.ng-pristine),
+                    &.ng-invalid.ng-touched {
+                        border-color: $ignite-brand-primary;
+                        box-shadow: inset 0 1px 3px 0 rgba($ignite-brand-primary, .5);
+                    }
+
+                    &:focus {
+                        border-color: $ignite-brand-success;
+                        box-shadow: inset 0 1px 3px 0 rgba($ignite-brand-success, .5);
+                    }
+
+                    &:disabled {
+                        opacity: .5;
+                    }
+
+
+                    &:focus {
+                        border-color: $ignite-brand-success;
+                        box-shadow: inset 0 1px 3px 0 rgba($ignite-brand-success, .5);
+                    }
+
+                    &:disabled {
+                        opacity: .5;
+                    }
+                }
+
+                & > input[type='number'] {
+                    text-align: left;
+                }
+            }
+
+            .tipField {
+                line-height: 36px;
+            }
+       }
+    }
+    .ignite-form-field__label {
+        float: left;
+        width: 100%;
+        margin: 0 0 2px;
+        padding: 0 10px;
+        height: 16px;
+        display: inline-flex;
+        align-items: center;
+
+        color: $gray-light;
+        font-size: 12px;
+        line-height: 12px;
+
+        &-disabled {
+            opacity: 0.5;   
+        }
+    }
+   .ignite-form-field__errors {
+        color: $ignite-brand-primary;
+        padding: 5px 10px 0;
+        line-height: 14px;
+        font-size: 12px;
+        clear: both;
+
+        &:empty {
+            display: none;
+        }
+
+        [ng-message] + [ng-message] {
+            margin-top: 10px;
+        }
+   }
+   @keyframes error-pulse {
+        from {
+            color: $ignite-brand-primary;
+        }
+        50% {
+            color: transparent;
+        }
+        to {
+            color: $ignite-brand-primary;
+        }
+   }
+   .ignite-form-field__error-blink {
+        .ignite-form-field__errors {
+            animation-name: error-pulse;
+            animation-iteration-count: 2;
+            animation-duration: 500ms;
+        }
+   }
+
+   .ignite-form-field.form-field-checkbox {
+        input[disabled] ~ * {
+            opacity: 0.5;
+        }
+   }
+}
+
+.form-field {
+    position: relative;
+    width: 100%;
+
+    &__label {
+        display: flex;
+        margin: 0 0 4px;
+
+        color: #424242;
+        font-size: 14px;
+        line-height: 1.25;
+
+        .icon-help {
+            line-height: 14px;
+        }
+
+        &.required:after {
+            content: '';
+        }
+
+        i {
+            font-style: normal;
+            color: $gray-light;
+        }
+
+        svg {
+            flex: 0 0 auto;
+            margin-left: 4px;
+        }
+
+        [ignite-icon='info'] {
+            position: relative;
+            top: 1px;
+
+            color: $ignite-brand-success;
+        }
+    }
+
+    &__control {
+        overflow: visible;
+        display: flex;
+        flex-direction: row;
+        width: 100%;
+
+        & > input::placeholder,
+        & > button.select-toggle.placeholder {
+            color: rgba(66, 66, 66, 0.5);
+            text-align: left;
+        }
+
+        & > input,
+        & > button:not(.btn-ignite) {
+            outline: none;
+            overflow: visible;
+
+            box-sizing: border-box;
+            width: 100%;
+            max-width: initial;
+            min-width: 0;
+            height: 36px;
+            padding: 9px 10px;
+            margin-right: 0;
+
+            border: solid 1px #c5c5c5;
+            border-radius: 4px;
+            background-color: #ffffff;
+            box-shadow: none;
+
+            color: $text-color;
+            font-size: 14px;
+            text-align: left;
+            line-height: 16px;
+
+            &.ng-invalid:not(.ng-pristine),
+            &.ng-invalid.ng-touched {
+                border-color: #c5c5c5;
+                box-shadow: none;
+            }
+
+            &.ng-invalid:focus,
+            &:focus {
+                border-color: $ignite-brand-success;
+                box-shadow: none;
+            }
+        }
+
+        &--postfix::after {
+            content: attr(data-postfix);
+            display: inline-flex;
+            align-self: center;
+            margin-left: 10px;
+        }
+
+        // Added right offset to appearance of input for invalid password
+        & > input[type='email'].ng-invalid.ng-touched,
+        & > input[type='text'].ng-invalid.ng-touched,
+        & > input[type='password'].ng-invalid.ng-touched {
+            padding-right: 36px;
+        }
+
+        // Added right offset to appearance of dropdown for invalid data
+        & > button.select-toggle.ng-invalid.ng-touched {
+            &:after {
+                right: 36px;
+            }
+        }
+    }
+
+    &__control-group {
+        input {
+            min-width: 0;
+            margin-right: -1px;
+
+            border-top-right-radius: 0 !important;
+            border-bottom-right-radius: 0 !important;
+
+            &:focus {
+                z-index: 1;
+            }
+        }
+
+        input + * {
+            border-top-left-radius: 0 !important;
+            border-bottom-left-radius: 0 !important;
+            flex: 0 0 auto;
+            width: 60px !important;
+        }
+    }
+
+    &__errors {
+        position: absolute;
+        right: 0;
+        bottom: 0;
+
+        display: flex;
+
+        [ng-message] {
+            // TODO: remove after replace all fields to new
+            overflow: visible !important;
+            animation: none !important;
+        }
+    }
+
+    &__control--postfix + &__errors::after {
+        content: attr(data-postfix);
+        margin-left: 10px;
+        visibility: hidden;
+    }
+
+    &__error {
+        z-index: 2;
+        position: relative;
+        width: 0;
+        height: 36px;
+        float: right;
+
+        color: $brand-primary;
+        line-height: $input-height;
+        pointer-events: initial;
+        text-align: center;
+
+        &:before {
+            position: absolute;
+            right: 0;
+            width: 38px;
+        }
+
+        div:first-child {
+            display: none;
+        }
+
+        [bs-tooltip] {
+            z-index: 1;
+            position: absolute;
+            top: 0;
+            right: 0;
+            width: 36px;
+            height: 36px;
+        }
+
+        [ignite-icon] {
+            position: absolute;
+            top: 10px;
+            right: 0;
+            width: 38px;
+        }
+    }
+
+    [disabled] {
+        opacity: .5;
+    }
+}
+
+.theme--ignite-errors-horizontal {
+    .form-field__control {
+        // Reset offset to appearance of input for invalid password
+        & > input[type='email'].ng-invalid.ng-touched,
+        & > input[type='text'].ng-invalid.ng-touched,
+        & > input[type='password'].ng-invalid.ng-touched {
+            padding-right: 0;
+        }
+        // Reset offset to appearance of dropdown for invalid data
+        & > button.select-toggle.ng-invalid.ng-touched {
+            &:after {
+                right: 10px;
+            }
+        }
+    }
+
+    .form-field__errors {
+        position: relative;
+        
+        padding: 5px 10px 0px;
+
+        color: $ignite-brand-primary;
+        font-size: 12px;
+        line-height: 14px;
+
+        &:empty {
+            display: none;
+        }
+
+        [ng-message] + [ng-message] {
+            margin-top: 10px;
+        }
+    }
+
+    .form-field__error {
+        float: none;
+        width: auto;
+        height: auto;
+        position: static;
+
+        text-align: left;
+        line-height: 14px;
+
+        div:first-child {
+            display: block;
+        }
+
+        [bs-tooltip],
+        [ignite-icon] {
+            display: none;
+        }
+    }
+
+    .form-field__error + .form-field__error {
+        margin-top: 10px;
+    }
+
+    .form-field__checkbox {
+        flex-wrap: wrap;
+
+        .form-field__errors {
+            margin-left: -10px;
+            flex-basis: 100%;
+
+            .form-field__error {
+                width: auto;
+
+                div {
+                    width: auto;
+                }
+            }
+        }
+    }
+}
+
+.form-field__radio,
+.form-field__checkbox {
+    $errorSize: 16px;
+    display: inline-flex;
+    width: auto;
+
+    сursor: pointer;
+
+    .form-field {
+        &__label {
+            order: 1;
+            margin: 0;
+            cursor: pointer;
+        }
+
+        &__control {
+            width: auto;
+            margin-right: 10px;
+            padding: 3px 0;
+            flex: 0 0 auto;
+
+            input {
+                width: auto;
+                height: auto;
+                margin: 0;
+                border-radius: 0;
+            }
+        }
+
+        &__errors {
+            position: static;
+            right: initial;
+            bottom: initial;
+            order: 3;
+            margin-left: 5px;
+
+            .form-field__error {
+                width: $errorSize;
+                height: $errorSize;
+
+                div {
+                    // Required to correctly position error popover
+                    top: -10px;
+                    height: 36px;
+                    
+                    width: $errorSize;                    
+                }
+
+                [ignite-icon] {
+                    width: $errorSize;
+                    top: 0;
+                }
+            }
+        }
+    }
+}
+
+.form-field__radio {
+    .form-field__control {
+        padding: 2px 0;
+    }
+
+    .form-field__control > input[type='radio'] {
+        -webkit-appearance: none;
+
+        width: 13px;
+        height: 13px;
+        padding: 0;
+
+        background: white;
+        border: none;
+        border-radius: 50%;
+        box-shadow: inset 0 0 0 1px rgb(197, 197, 197);
+
+        &:focus {
+            outline: none;
+            border: none;
+            box-shadow: 0 0 0 2px rgba(0, 103, 185, .3),
+                  inset 0 0 0 1px rgb(197, 197, 197);
+        }
+
+        &:checked {
+            border: none;
+            box-shadow: inset 0 0 0 5px rgba(0, 103, 185, 1); 
+
+            &:focus {
+                box-shadow: 0 0 0 2px rgba(0, 103, 185, .3),
+                      inset 0 0 0 5px rgba(0, 103, 185, 1); 
+            }
+        }
+    }
+}
+
+.form-field__checkbox {
+    .form-field__control > input[type='checkbox'] {
+        border-radius: 2px;
+
+        background-image: url(/images/checkbox.svg);
+        width: 12px !important;
+        height: 12px !important;
+        -webkit-appearance: none;
+        -moz-appearance: none;
+        appearance: none;
+        background-repeat: no-repeat;
+        background-size: 100%;
+        padding: 0;
+        border: none;
+
+        &:checked {
+            background-image: url(/images/checkbox-active.svg);
+        }
+
+        &:disabled {
+            opacity: 0.5;
+        }
+
+        &:focus {
+            outline: none;
+            box-shadow: 0 0 0 2px rgba(0, 103, 185, .3);
+        }
+    }
+}
+
+.form-field--inline {
+    display: inline-block;
+    width: auto;
+
+    .form-field {
+        display: flex;
+        align-items: baseline;
+    }
+
+    .form-field__label {
+        white-space: nowrap;
+    }
+
+    form-field-size,
+    .form-field__text {
+        .form-field__control {
+            margin-left: 10px;
+        }
+    }
+
+    .form-field__dropdown,
+    .form-field__datepicker,
+    .form-field__timepicker {
+        .form-field__control {
+            width: auto;
+
+            input,
+            button {
+                color: transparent;
+
+                text-shadow: 0 0 0 $ignite-brand-success;
+
+                border: none;
+                box-shadow: none;
+
+                background: linear-gradient(to right, rgb(0, 103, 185), transparent) 0px 25px / 0px, 
+                            linear-gradient(to right, rgb(0, 103, 185) 70%, transparent 0%) 0% 0% / 8px 1px repeat-x,
+                            0% 0% / 0px, 0% 0% / 4px;
+                background-size: 0, 8px 1px, 0, 0;
+                background-position: 1px 25px;
+
+                padding-left: 0px;
+                padding-right: 0px;
+                margin-left: 10px;
+                margin-right: 10px;
+
+                &:hover, &:focus {
+                    text-shadow: 0 0 0 change-color($ignite-brand-success, $lightness: 26%);
+                }
+            }
+        }
+    }
+
+    .form-field__dropdown {
+        button::after {
+            display: none;
+        }
+    }
+}
+
+.form-field__password {
+    // Validation error notification will overlap with visibility button if it's not moved more to the left
+    input[type='password'].ng-invalid.ng-touched,
+    input[type='password'].ng-invalid.ng-touched + input {
+        padding-right: 62px;
+    }
+
+    // Extra space for visibility button
+    input {
+        padding-right: 36px;
+    }
+
+    // Distance between error notification and visibility button
+    .form-field__errors {
+        right: 26px;
+    }
+
+    password-visibility-toggle-button {
+        position: absolute;
+        right: 0;
+        height: 36px;
+    }
+}
+
+.form-field__dropdown {
+    .form-field__control {
+        > button:not(.btn-ignite) {
+            padding-top: 10px;
+        }
+    }
+}
+
+.form-field__ace {
+    .ace_editor {
+        width: 100%;
+        min-height: 70px;
+        margin: 0;
+
+        border: solid 1px #c5c5c5;
+        border-radius: 4px;
+        background-color: #ffffff;
+        box-shadow: none;
+
+        .ace_content {
+            padding-left: 2px;
+        }
+
+        &.ace_focus {
+            border-color: $ignite-brand-success;
+            box-shadow: none;
+        }
+    }
+}
+
+.form-field.ignite-form-field label.required {
+    margin-left: 0 !important;
+}
+
+.form-fieldset {
+    padding: 10px;
+
+    border: 1px solid hsla(0, 0%, 77%, .5);
+    border-radius: 4px;
+
+    legend {
+        width: auto;
+        margin: 0 -5px;
+        padding: 0 5px;
+
+        border: 0;
+
+        color: #393939;
+        font-size: 14px;
+        line-height: 1.42857;
+    }
+
+    legend + * {
+        margin-top: 0 !important;
+    }
+
+    & > *:last-child {
+        margin-bottom: 0 !important;
+    }
+}
diff --git a/modules/frontend/app/primitives/form-field/input.pug b/modules/frontend/app/primitives/form-field/input.pug
new file mode 100644
index 0000000..0551101
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/input.pug
@@ -0,0 +1,29 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__input({ name, model, disabled, required, placeholder, namePostfix = '' })
+    input(
+        id=`{{ ${name} }}${ namePostfix }Input`
+        name=`{{ ${name} }}`
+        placeholder=placeholder
+
+        ng-model=model
+
+        ng-required=required && `${required}`
+        ng-disabled=disabled && `${disabled}`
+        ng-ref='$input'
+        ng-ref-read='ngModel'
+    )&attributes(attributes ? attributes.attributes ? attributes.attributes : attributes : {})
diff --git a/modules/frontend/app/primitives/form-field/label.pug b/modules/frontend/app/primitives/form-field/label.pug
new file mode 100644
index 0000000..74ddecb
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/label.pug
@@ -0,0 +1,31 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__label({ label, name, required, optional, disabled, namePostfix = '' })
+    if label
+        -var colon = label[label.length-1] === ':' ? ':' : '';
+        - label = label[label.length-1] === ':' ? label.substring(0, label.length - 1) : label
+        - optional = optional ? ' <i>(optional)</i>' : '';
+
+        label.form-field__label(
+            id=name && `{{ ${name} }}Label`
+            for=name && `{{ ${name} }}${ namePostfix }Input`
+            class=`{{ ${required} ? 'required' : '' }}`
+            ng-disabled=disabled && `${disabled}`
+        )
+            span !{label}!{optional}!{colon}
+            if block
+                block
diff --git a/modules/frontend/app/primitives/form-field/number.pug b/modules/frontend/app/primitives/form-field/number.pug
new file mode 100644
index 0000000..dfcfff3
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/number.pug
@@ -0,0 +1,48 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__number({ label, model, name, disabled, required, placeholder, tip, min, max, step = '1', postfix })
+    -var errLbl = label[label.length - 1] === ':' ? label.substring(0, label.length - 1) : label
+
+    .form-field.ignite-form-field
+        +form-field__label({ label, name, required, disabled })
+            +form-field__tooltip({ title: tip, options: tipOpts })
+
+        .form-field__control(class=postfix && 'form-field__control--postfix' data-postfix=postfix)
+            - attributes.type = 'number'
+            - attributes.min = min ? min : '0'
+            - attributes.max = max ? max : '{{ Number.MAX_VALUE }}'
+            - attributes.step = step
+            +form-field__input({ name, model, disabled, required, placeholder })(attributes=attributes)
+
+        .form-field__errors(
+            data-postfix=postfix
+            ng-messages=`$input.$error`
+            ng-show=`($input.$dirty || $input.$touched || $input.$submitted) && $input.$invalid`
+        )
+            if required
+                +form-field__error({ error: 'required', message: `${errLbl} could not be empty!` })
+
+            +form-field__error({ error: 'min', message: `${errLbl} is less than allowable minimum: ${ min || 0 }`})
+
+            +form-field__error({ error: 'max', message: `${errLbl} is more than allowable maximum: ${ max }`})
+
+            +form-field__error({ error: 'step', message: `${errLbl} step should be ${step || 1}` })
+
+            +form-field__error({ error: 'number', message: 'Only numbers allowed' })
+
+            if block
+                block
diff --git a/modules/frontend/app/primitives/form-field/password.pug b/modules/frontend/app/primitives/form-field/password.pug
new file mode 100644
index 0000000..5103281
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/password.pug
@@ -0,0 +1,53 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__password({ label, model, name, disabled, required, placeholder, tip })
+    -var errLbl = label.substring(0, label.length - 1)
+
+    .form-field.form-field__password.ignite-form-field(
+        password-visibility-root
+        on-password-visibility-toggle=`$input1.$setTouched(); $input2.$setTouched()`
+    )
+        +form-field__label({ label, name, required })
+            +form-field__tooltip({ title: tip, options: tipOpts })
+
+        .form-field__control
+            - attributes.type='password'
+            - attributes.class = 'password-visibility__password-hidden'
+            - attributes['ng-ref'] = '$input1'
+            +form-field__input({ name, model, disabled, required, placeholder })(attributes=attributes)
+            - attributes.class = 'password-visibility__password-visible'
+            - attributes.type='text'
+            - attributes.autocomplete = 'off'
+            - attributes['ng-ref'] = '$input2'
+            +form-field__input({ name: name + `+"Text"`, model, disabled, required, placeholder })(attributes=attributes)
+
+            password-visibility-toggle-button
+
+        .form-field__errors(
+            ng-messages=`$input1.$error || $input2.$error`
+            ng-show=`
+                ($input1.$dirty || $input1.$touched || $input1.$submitted) && $input1.$invalid ||
+                ($input2.$dirty || $input2.$touched || $input2.$submitted) && $input2.$invalid
+            `
+        )
+            if required
+                +form-field__error({ error: 'required', message: `${errLbl} could not be empty!` })
+
+            +form-field__error({ error: 'mismatch', message: `Password does not match the confirm password!` })
+
+            if block
+                block
diff --git a/modules/frontend/app/primitives/form-field/phone.pug b/modules/frontend/app/primitives/form-field/phone.pug
new file mode 100644
index 0000000..4c79477
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/phone.pug
@@ -0,0 +1,37 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__phone({ label, model, name, disabled, required, optional, placeholder, tip })
+    -var errLbl = label.substring(0, label.length - 1)
+
+    .form-field.ignite-form-field
+        +form-field__label({ label, name, required, optional })
+            +form-field__tooltip({ title: tip, options: tipOpts })
+
+        .form-field__control
+            - attributes.type='tel'
+            +form-field__input({ name, model, disabled, required, placeholder })(attributes=attributes)
+
+        .form-field__errors(
+            ng-messages=`$input.$error`
+            ng-show=`($input.$dirty || $input.$touched || $input.$submitted) && $input.$invalid`
+        )
+            if required
+                +form-field__error({ error: 'required', message: `${errLbl} could not be empty!` })
+
+            if block
+                block
+
diff --git a/modules/frontend/app/primitives/form-field/radio.pug b/modules/frontend/app/primitives/form-field/radio.pug
new file mode 100644
index 0000000..57ae097
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/radio.pug
@@ -0,0 +1,32 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__radio({ label, model, name, value, disabled, required, tip })
+    .form-field.form-field__radio
+        +form-field__label({ label, name, required, disabled, namePostfix: value })
+            +form-field__tooltip({ title: tip, options: tipOpts })
+
+        .form-field__control
+            - attributes.type='radio'
+            - attributes['ng-value'] = value
+            +form-field__input({ name, model, disabled, required, placeholder, namePostfix: value })(attributes=attributes)
+
+        .form-field__errors(
+            ng-messages=`$input.$error`
+            ng-show=`($input.$dirty || $input.$touched || $input.$submitted) && $input.$invalid`
+        )
+            if block
+                block
diff --git a/modules/frontend/app/primitives/form-field/text.pug b/modules/frontend/app/primitives/form-field/text.pug
new file mode 100644
index 0000000..5a2595e
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/text.pug
@@ -0,0 +1,36 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__text({ label, model, name, disabled, required, placeholder, tip })
+    -let errLbl = label[label.length - 1] === ':' ? label.substring(0, label.length - 1) : label
+
+    .form-field.form-field__text.ignite-form-field(id=`{{ ${name} }}Field`)
+        +form-field__label({ label, name, required, disabled })
+            +form-field__tooltip({ title: tip, options: tipOpts })
+
+        .form-field__control
+            - attributes.type='text'
+            +form-field__input({ name, model, disabled, required, placeholder })(attributes=attributes)
+
+        .form-field__errors(
+            ng-messages=`$input.$error`
+            ng-show=`($input.$dirty || $input.$touched || $input.$submitted) && $input.$invalid`
+        )
+            if required
+                +form-field__error({ error: 'required', message: `${errLbl} could not be empty!` })
+
+            if block
+                block
diff --git a/modules/frontend/app/primitives/form-field/tooltip.pug b/modules/frontend/app/primitives/form-field/tooltip.pug
new file mode 100644
index 0000000..34376fd
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/tooltip.pug
@@ -0,0 +1,26 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__tooltip({ title, options = { placement: 'auto' }})
+    if title
+        svg(
+            ignite-icon='info'
+            bs-tooltip=''
+
+            data-title=title
+            data-container=options && options.container || false
+            data-placement=options && options.placement || false
+        )&attributes(attributes)
diff --git a/modules/frontend/app/primitives/form-field/typeahead.pug b/modules/frontend/app/primitives/form-field/typeahead.pug
new file mode 100644
index 0000000..ee2312f
--- /dev/null
+++ b/modules/frontend/app/primitives/form-field/typeahead.pug
@@ -0,0 +1,55 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__typeahead({ label, model, name, disabled, required, placeholder, options, tip })
+    -var errLbl = label.substring(0, label.length - 1)
+    mixin __form-field__typeahead()
+        input(
+            id=`{{ ${name} }}Input`
+            name=`{{ ${name} }}`
+            placeholder=placeholder
+           
+            ng-model=model
+
+            ng-required=required && `${required}`
+            ng-disabled=disabled && `${disabled}` || `!${options}.length`
+
+            bs-typeahead
+            bs-options=`item for item in ${options}`
+            container='body'
+            data-min-length='1'
+            ignite-retain-selection
+            ng-ref='$input'
+            ng-ref-read='ngModel'
+        )&attributes(attributes.attributes)
+
+    .form-field.form-field__typeahead.ignite-form-field(id=`{{ ${name} }}Field`)
+        +form-field__label({ label, name, required, disabled })
+            +form-field__tooltip({ title: tip, options: tipOpts })
+
+        .form-field__control
+            - attributes.type='text'
+            +__form-field__typeahead(attributes=attributes)
+
+        .form-field__errors(
+            ng-messages=`$input.$error`
+            ng-show=`($input.$dirty || $input.$touched || $input.$submitted) && $input.$invalid`
+        )
+            if required
+                +form-field__error({ error: 'required', message: `${errLbl} could not be empty!` })
+
+            if block
+                block
diff --git a/modules/frontend/app/primitives/grid/index.scss b/modules/frontend/app/primitives/grid/index.scss
new file mode 100644
index 0000000..a77240f
--- /dev/null
+++ b/modules/frontend/app/primitives/grid/index.scss
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.theme--ignite {
+    .row {
+        display: flex;
+        margin: 16px -10px;
+
+        line-height: 36px;
+
+        & > [class*="col-"] {
+            & > * {
+                align-self: flex-end;
+            }
+        }
+    }
+
+    [class*="col-"] {
+        display: inline-flex;
+        padding: 0 10px;
+    }
+
+    @for $i from 1 through 20 {
+        .col-#{$i*5} {
+            width: $i*5%;
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/primitives/index.js b/modules/frontend/app/primitives/index.js
new file mode 100644
index 0000000..a9ff053
--- /dev/null
+++ b/modules/frontend/app/primitives/index.js
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './badge/index.scss';
+import './btn/index.scss';
+import './datepicker/index.scss';
+import './timepicker/index.scss';
+import './tabs/index.scss';
+import './table/index.scss';
+import './panel/index.scss';
+import './dropdown/index.scss';
+import './modal/index.scss';
+import './ui-grid/index.scss';
+import './ui-grid-header/index.scss';
+import './ui-grid-settings/index.scss';
+import './page/index.scss';
+import './spinner-circle/index.scss';
+import './switcher/index.scss';
+import './form-field/index.scss';
+import './typography/index.scss';
+import './grid/index.scss';
+import './checkbox/index.scss';
+import './tooltip/index.scss';
diff --git a/modules/frontend/app/primitives/modal/index.scss b/modules/frontend/app/primitives/modal/index.scss
new file mode 100644
index 0000000..8668524
--- /dev/null
+++ b/modules/frontend/app/primitives/modal/index.scss
@@ -0,0 +1,269 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../../public/stylesheets/variables';
+
+.modal {
+    display: block;
+    overflow: hidden;
+
+    &.center {
+        display: flex !important;
+        flex-direction: column;
+        justify-content: center;
+    }
+}
+
+.modal .close {
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    float: none;
+}
+
+.modal-header {
+    border-top-left-radius: 6px;
+    border-top-right-radius: 6px;
+
+    // Close icon
+    .close {
+        margin-right: -2px;
+    }
+
+    // Modal icon
+    h4 > i {
+        cursor: default;
+        float: left;
+        line-height: $modal-title-line-height;
+    }
+}
+
+.modal .modal-dialog {
+    width: 650px;
+}
+
+.modal .modal-dialog--adjust-height {
+    margin-top: 0;
+    margin-bottom: 0;
+
+    .modal-body {
+        max-height: calc(100vh - 150px);
+        overflow: auto;
+    }
+}
+
+.modal .modal-content .modal-header {
+    background-color: $ignite-background-color;
+    text-align: center;
+    color: $ignite-header-color;
+    padding: 15px 25px 15px 15px;
+}
+
+.modal .modal-content .modal-header h4 {
+    font-size: 22px;
+}
+
+.modal .modal-content .modal-footer {
+    margin-top: 0;
+}
+
+.modal-footer {
+    label {
+        float: left;
+        margin: 0;
+    }
+
+    .btn:last-child {
+        margin-right: 0;
+    }
+
+    .checkbox {
+        margin: 0;
+    }
+}
+
+.modal-body {
+    margin-left: 20px;
+    margin-right: 20px;
+}
+
+.modal-body-with-scroll {
+    max-height: 420px;
+    overflow-y: auto;
+    overflow-y: overlay;
+    margin: 0;
+}
+
+.modal--ignite {
+    .close {
+        position: absolute;
+        top: 20px;
+        right: 20px;
+
+        display: block;
+
+        opacity: 1;
+        background: none;
+        color: $gray-light;
+
+        [ignite-icon] {
+            height: 12px;
+        }
+
+        &:hover, &:focus {
+            color: $text-color;
+        }
+    }
+
+    .modal-content {
+        border: none;
+        border-radius: 4px;
+        background-color: white;
+
+        box-shadow: 0 2px 4px 0 rgba(35, 36, 40, 0.5);
+
+        .modal-header {
+            display: flex;
+            align-items: center;
+
+            padding: 10px 20px;
+
+            text-align: left;
+
+            border-top-left-radius: 4px;
+            border-top-right-radius: 4px;
+            border-bottom: 1px solid $table-border-color;
+
+            h4 {
+                display: inline-flex;
+                align-items: center;
+                margin-top: 1px;
+                margin-bottom: -1px;
+
+                color: $text-color;
+                font-size: 16px;
+                font-weight: 400;
+                line-height: 36px;
+
+                i, .icon-left, svg {
+                    margin-right: 8px;
+
+                    color: #424242;
+                    font-size: 18px;
+                    line-height: inherit;
+                }
+            }
+        }
+
+        .modal-body {
+            padding: 5px 20px;
+            margin: 0;
+
+            .input-tip {
+                padding-top: 0;
+            }
+
+            .modal-body--inner-content {
+                max-height: 300px;
+                overflow-x: auto;
+
+                p {
+                    text-align: left;
+                    white-space: nowrap;
+                }
+            }
+        }
+
+        .modal-footer {
+            padding: 10px 20px;
+
+            border-top: 1px solid $table-border-color;
+
+            button + button,
+            button + a {
+                margin-left: 20px;
+            }
+        }
+
+        ul.tabs {
+            margin-top: 20px;
+        }
+
+        .panel--ignite {
+            margin-top: 20px;
+            margin-bottom: 20px;
+        }
+
+        ul.tabs + .panel--ignite {
+            margin-top: 0;
+        }
+
+        fieldset {
+            margin: 16px 0;
+
+            & > .row:first-child {
+                margin-top: 0;
+            }
+        }
+    }
+}
+
+.modal-footer {
+    display: flex;
+    align-items: center;
+
+    > div {
+        display: flex;
+        flex: 1;
+
+        &:last-child {
+            flex: 1;
+            justify-content: flex-end;
+
+            &.modal-footer--no-grow {
+                flex-grow: 0;
+            }
+        }
+    }
+}
+
+.modal--wide .modal-dialog {
+    width: 900px;
+}
+
+.modal-with-ace {
+    .modal-body {
+        padding-left: 5px !important;
+    }
+
+    .modal-dialog {
+        transform: none !important;
+
+        .ace_warning:before, .ace_error:before {
+            position: absolute !important;
+            left: -7px !important;
+        }
+    }
+
+    .ace_gutter, .ace_layer {
+        overflow: visible !important;
+    }
+
+    .ace_gutter {
+        padding-left: 5px !important;
+    }
+}
diff --git a/modules/frontend/app/primitives/page/index.scss b/modules/frontend/app/primitives/page/index.scss
new file mode 100644
index 0000000..7122835
--- /dev/null
+++ b/modules/frontend/app/primitives/page/index.scss
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.docs-content {
+    header {
+        margin: 40px 0 30px;
+        border: none;
+        background-color: initial;
+
+        h1 {
+            color: #393939;
+            font-size: 24px;
+            font-weight: normal;
+            font-style: normal;
+            font-stretch: normal;
+            line-height: 24px;
+            letter-spacing: normal;
+        }
+    }
+}
diff --git a/modules/frontend/app/primitives/panel/index.scss b/modules/frontend/app/primitives/panel/index.scss
new file mode 100644
index 0000000..9449fdb
--- /dev/null
+++ b/modules/frontend/app/primitives/panel/index.scss
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../../public/stylesheets/variables';
+
+.panel {
+    &--ignite {
+        border: none;
+        border-radius: 0 0 4px 4px;
+
+        background-color: white;
+        box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
+
+        & > header {
+            height: auto;
+            padding: 22px 20px;
+
+            background-color: initial;
+            font-size: 16px;
+
+            &:hover {
+                text-decoration: none;
+            }
+
+            h5 {
+                margin: 0;
+                font-size: 16px;
+                font-weight: normal;
+                line-height: 36px;
+            }
+        }
+
+        & > hr {
+            margin: 0;
+            border-top: 1px solid #ddd;
+        }
+
+        & > section {
+            padding: 0 20px;
+        }
+
+        & > .panel-heading {
+            height: auto;
+            padding: 22px 20px;
+
+            background-color: initial;
+            border-bottom: 1px solid $table-border-color;
+
+            &:hover {
+                text-decoration: none;
+            }
+
+            & > .panel-title {
+                font-size: 16px;
+                line-height: 36px;
+
+                & > .panel-selected {
+                    font-size: 14px;
+                    font-style: italic;
+                    line-height: 36px;
+                    cursor: default;
+                }
+            }
+        }
+
+        & > header.header-with-selector {
+            margin: 0;
+            padding: 14px 20px;
+            min-height: 65px;
+
+            border-bottom: 1px solid #ddd;
+
+            sub {
+                bottom: 0;
+            }
+        }
+    }
+
+    &--collapse {
+        & > header {
+            cursor: pointer;
+        }
+
+        [ignite-icon='expand'],
+        [ignite-icon='collapse'] {
+            width: 13px;
+            height: 13px;
+
+            margin-right: 9px;
+        }
+
+        [ignite-icon='expand'] {
+            display: none;
+        }
+
+        [ignite-icon='collapse'] {
+            display: inline-block;
+        }
+
+        &.in {
+            [ignite-icon='expand'] {
+                display: inline-block;
+            }
+
+            [ignite-icon='collapse'] {
+                display: none;
+            }
+        }
+    }
+}
+
+// Adding top border for panels in modals
+.modal-body {
+    .panel--ignite:not(.panel--ignite__without-border) {
+        border-top: 1px solid #d4d4d4;
+        border-radius: 4px 4px 0 0 ;
+    }
+}
diff --git a/modules/frontend/app/primitives/spinner-circle/index.scss b/modules/frontend/app/primitives/spinner-circle/index.scss
new file mode 100644
index 0000000..88152fa
--- /dev/null
+++ b/modules/frontend/app/primitives/spinner-circle/index.scss
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+$color-inactive-primary: #C5C5C5;
+$color-inactive-secondary: #FFFFFF;
+$color-active-primary: #EE2B27;
+$color-active-secondary: #FF8485;
+
+.spinner-circle {
+  display: inline-block;
+
+  &:before {
+    content: '';
+
+    display: block;
+
+    width: 20px;
+    height: 20px;
+
+    border-width: 1px;
+    border-style: solid;
+    border-radius: 50%;
+    border-color: $color-inactive-primary;
+    border-left-color: $color-active-primary;
+  }
+}
+
+.spinner-circle:before {
+  border-left-width: 2px;
+  border-left-color: $color-active-primary;
+
+  animation-name: spinner-circle--animation;
+  animation-duration: 1s;
+  animation-iteration-count: infinite;
+  animation-timing-function: linear;
+}
+
+@keyframes spinner-circle--animation {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
diff --git a/modules/frontend/app/primitives/switcher/index.pug b/modules/frontend/app/primitives/switcher/index.pug
new file mode 100644
index 0000000..8b7d009
--- /dev/null
+++ b/modules/frontend/app/primitives/switcher/index.pug
@@ -0,0 +1,20 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin switcher()
+    label.switcher--ignite
+        input(type='checkbox')&attributes(attributes)
+        div(bs-tooltip=attributes.tip && '' data-title=attributes.tip data-trigger='hover' data-placement='bottom')
diff --git a/modules/frontend/app/primitives/switcher/index.scss b/modules/frontend/app/primitives/switcher/index.scss
new file mode 100644
index 0000000..9430f71
--- /dev/null
+++ b/modules/frontend/app/primitives/switcher/index.scss
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../../public/stylesheets/variables';
+
+label.switcher--ignite {
+    $width: 34px;
+    $height: 20px;
+
+    $color-inactive-primary: #c5c5c5;
+    $color-inactive-secondary: #ffffff;
+    $color-active-primary: $ignite-brand-primary;
+    $color-active-secondary: #ff8485;
+
+    width: $width;
+    max-width: $width !important;
+    height: $height;
+
+    line-height: $height;
+    vertical-align: middle;
+
+    cursor: pointer;
+
+    input[type='checkbox'] {
+        position: absolute;
+        opacity: 0.0;
+
+        & + div {
+            position: relative;
+
+            width: $width;
+            height: 14px;
+            margin: 3px 0;
+
+            border-radius: 8px;
+            background-color: $color-inactive-primary;
+            transition: background 0.2s ease;
+
+            &:before {
+                content: '';
+
+                position: absolute;
+                top: -3px;
+                left: 0;
+
+                width: $height;
+                height: $height;
+
+                border-width: 1px;
+                border-style: solid;
+                border-radius: 50%;
+                border-color: $color-inactive-primary;
+                background-color: $color-inactive-secondary;
+
+                transition: all 0.12s ease;
+            }
+
+            &:active:before {
+                transform: scale(1.15, 0.85);
+            }
+        }
+
+        &[is-in-progress='true'] + div:before {
+            border-left-width: 2px;
+            border-left-color: $color-active-primary;
+
+            animation-name: switcher--animation;
+            animation-duration: 1s;
+            animation-iteration-count: infinite;
+            animation-timing-function: linear;
+        }
+
+        &:checked + div {
+            background-color: $color-active-secondary;
+
+            &:before {
+                content: '';
+
+                left: 14px;
+
+                border-color: $color-active-primary;
+                background-color: $color-active-primary;
+            }
+        }
+
+        &[is-in-progress='true']:checked + div {
+            background-color: $color-inactive-primary;
+
+            &:before {
+                border-color: $color-inactive-primary;
+                border-left-color: $color-active-primary;
+                background-color: $color-inactive-secondary;
+            }
+        }
+    }
+}
+
+@keyframes switcher--animation {
+    from {
+        transform: rotate(0deg);
+    }
+    to {
+        transform: rotate(360deg);
+    }
+}
diff --git a/modules/frontend/app/primitives/table/index.scss b/modules/frontend/app/primitives/table/index.scss
new file mode 100644
index 0000000..e3f98f8
--- /dev/null
+++ b/modules/frontend/app/primitives/table/index.scss
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../../public/stylesheets/variables';
+
+table.table--ignite {
+    width: 100%;
+
+    line-height: 46px;
+
+    th, td {
+        padding: 0 20px;
+
+        border: solid $table-border-color;
+        border-width: 0;
+
+        &:first-child {
+            border-left-width: 1px;
+        }
+
+        &:last-child {
+            border-right-width: 1px;
+        }
+    }
+
+    th {
+        border-right-width: 1px;
+    }
+
+    td {
+        border-bottom-width: 1px;
+    }
+
+    thead {
+        border: solid $table-border-color;
+        border-width: 1px 0;
+
+        th {
+            color: $gray-light;
+            font-weight: 400;
+        }
+    }
+
+    tbody {
+        tr {
+            &:nth-child(even) {
+                background-color: $body-bg;
+            }
+        }
+
+        td {
+            color: $text-color;
+
+            .text-overflow {
+                overflow: hidden;
+                max-width: 220px;
+                text-overflow: ellipsis;
+                white-space: nowrap;
+            }
+        }
+    }
+
+    .st-sort-ascent:after,
+    .st-sort-descent:after {
+        margin-left: 5px;
+        font-family: "ui-grid";
+    }
+
+    .st-sort-ascent:after {
+        content: '\C359';
+    }
+
+    .st-sort-descent:after {
+        content: '\C358';
+    }
+}
diff --git a/modules/frontend/app/primitives/tabs/index.scss b/modules/frontend/app/primitives/tabs/index.scss
new file mode 100644
index 0000000..f63ae9b
--- /dev/null
+++ b/modules/frontend/app/primitives/tabs/index.scss
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../../public/stylesheets/variables';
+
+ul.tabs {
+    $width: auto;
+    $height: 40px;
+    $offset-vertical: 11px;
+    $offset-horizontal: 25px;
+    $font-size: 14px;
+    $border-width: 5px;
+
+    list-style: none;
+
+    padding-left: 0;
+    margin-bottom: 0;
+
+    border-bottom: 1px solid $nav-tabs-border-color;
+
+    li {
+        position: relative;
+        top: 1px;
+        height: $height + $border-width;
+
+        display: inline-block;
+
+        border-bottom: 0px solid transparent;
+        transition-property: border-bottom;
+        transition-duration: 0.2s;
+
+        a {
+            display: inline-block;
+            width: $width;
+            height: $height;
+
+            padding: $offset-vertical $offset-horizontal;
+
+            color: $text-color;
+            font-size: $font-size;
+            text-align: center;
+            line-height: $height - 2*$offset-vertical;
+
+            &:hover, &:focus {
+              text-decoration: none;
+            }
+
+            .badge {
+              margin-left: $offset-vertical;
+            }
+        }
+
+        &.active, &:hover {
+            border-bottom-width: $border-width;
+        }
+
+        &.active {
+            border-color: $brand-primary;
+        }
+
+        &:not(.active):hover {
+            border-color: lighten($brand-primary, 25%);
+        }
+
+        & + li {
+            margin-left: 45px;
+        }
+    }
+}
+
+ul.tabs.tabs--blue {
+    li {
+        &.active {
+            border-color: #0067b9;
+        }
+
+        &:not(.active):hover {
+            border-color: #94bbdd;
+        }
+    }
+}
\ No newline at end of file
diff --git a/modules/frontend/app/primitives/timepicker/index.pug b/modules/frontend/app/primitives/timepicker/index.pug
new file mode 100644
index 0000000..9a4dd97
--- /dev/null
+++ b/modules/frontend/app/primitives/timepicker/index.pug
@@ -0,0 +1,63 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+mixin form-field__timepicker({ label, model, name, mindate, maxdate, disabled, required, placeholder, tip, format = 'HH:mm'})
+    mixin __form-field__timepicker()
+        input(
+            id=`{{ ${name} }}Input`
+            name=`{{ ${name} }}`
+
+            placeholder=placeholder
+            
+            ng-model=model
+
+            ng-required=required && `${required}`
+            ng-disabled=disabled && `${disabled}`
+
+            bs-timepicker
+            data-time-format=format
+            data-length='1'
+            data-minute-step='1'
+            data-second-step='1'
+            data-arrow-behavior='picker'
+
+            data-placement='bottom'
+            data-container='body'
+
+            tabindex='0'
+
+            onkeydown='return false'
+            ng-ref='$input'
+            ng-ref-read='ngModel'
+        )&attributes(attributes.attributes)
+
+    .form-field.form-field__timepicker.ignite-form-field(id=`{{ ${name} }}Field`)
+        +form-field__label({ label, name, required, disabled })
+            +form-field__tooltip({ title: tip, options: tipOpts })
+
+        .form-field__control
+            - attributes.type='button'
+            +__form-field__timepicker(attributes=attributes)
+
+        .form-field__errors(
+            ng-messages=`$input.$error`
+            ng-show=`($input.$dirty || $input.$touched || $input.$submitted) && $input.$invalid`
+        )
+            if required
+                +form-field__error({ error: 'required', message: `${errLbl} could not be empty!` })
+
+            if block
+                block
diff --git a/modules/frontend/app/primitives/timepicker/index.scss b/modules/frontend/app/primitives/timepicker/index.scss
new file mode 100644
index 0000000..1b21c70
--- /dev/null
+++ b/modules/frontend/app/primitives/timepicker/index.scss
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.timepicker.dropdown-menu {
+    padding: 0 4px;
+    line-height: 30px;
+    z-index: 2000;
+
+    button {
+        outline: none;
+        border: 0;
+    }
+
+    tbody button {
+        height: 100%;
+        padding: 6px;
+    }
+
+    thead, tfoot {
+        th {
+            text-align: center;
+            line-height: 0;
+        }
+
+        .btn.btn-default {
+            float: none !important;
+            display: inline-block;
+            margin-right: 0;
+
+            &:active {
+                box-shadow: none;
+                background: none;
+            }
+
+            &:before {
+                content: '';
+
+                display: block;
+                width: 10px;
+                height: 10px;
+
+                border: 2px solid #757575;
+                transform: rotate(45deg);
+            }
+
+            &:hover {
+                background: none;
+            }
+        }
+    }
+
+    thead {
+        th {
+            padding-top: 10px;
+        }
+
+        .btn.btn-default {
+            &:before {
+                border-width: 2px 0 0 2px;
+            }
+        }
+    }
+
+    tfoot {
+        th {
+            padding-top: 2px;
+            padding-bottom: 10px;
+        }
+
+        .btn.btn-default {
+            &:before {
+                border-width: 0 2px 2px 0;
+            }
+        }
+    }
+}
diff --git a/modules/frontend/app/primitives/tooltip/index.scss b/modules/frontend/app/primitives/tooltip/index.scss
new file mode 100644
index 0000000..27f30ed
--- /dev/null
+++ b/modules/frontend/app/primitives/tooltip/index.scss
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.tooltip {
+    & > .tooltip-inner {
+        padding: 12px;
+        background-color: #FFF;
+    }
+}
+
+.tooltip--error {
+    &.top {
+        margin-top: 8px;
+        margin-left: -1px;
+    }
+
+    &.top .tooltip-arrow {
+        border-top-color: #f34718;
+    }
+
+    .tooltip-inner {
+        background: #f34718;
+        border-color: #f34718;
+        color: white;
+    }
+}
diff --git a/modules/frontend/app/primitives/typography/index.scss b/modules/frontend/app/primitives/typography/index.scss
new file mode 100644
index 0000000..9baa444
--- /dev/null
+++ b/modules/frontend/app/primitives/typography/index.scss
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../../public/stylesheets/variables';
+
+.theme--ignite {
+    p {
+        margin: 20px 0;
+
+        color: $text-color;
+        font-size: 14px;
+        line-height: 17px;
+    }
+
+    h4 {
+        margin-top: 20px;
+
+        color: $text-color;
+        font-size: 16px;
+        font-weight: normal;
+    }
+}
diff --git a/modules/frontend/app/primitives/ui-grid-header/index.scss b/modules/frontend/app/primitives/ui-grid-header/index.scss
new file mode 100644
index 0000000..7c2efc3
--- /dev/null
+++ b/modules/frontend/app/primitives/ui-grid-header/index.scss
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../../public/stylesheets/variables';
+
+.ui-grid-header--subcategories {
+    border-color: $table-border-color;
+
+    .ui-grid-header-canvas {
+        background-color: #f5f5f5;
+    }
+
+    .ui-grid-row:nth-child(even) .ui-grid-cell.cell-total {
+        background-color: rgba(102,175,233,.6);
+    }
+
+    .ui-grid-row:nth-child(odd) .ui-grid-cell.cell-total {
+        background-color: rgba(102,175,233,.3);
+    }
+
+    .ui-grid-header-cell-row {
+        height: 30px;
+    }
+
+    .ui-grid-header-cell {
+        // Workaround: Fixed cell header offset in IE11.
+        vertical-align: top;
+
+        .ui-grid-cell-contents > span:not(.ui-grid-header-cell-label) {
+            right: 3px;
+        }
+    }
+
+    .ui-grid-header-cell:last-child .ui-grid-column-resizer.right {
+        border-color: $table-border-color;
+    }
+
+    .ui-grid-header-cell [role="columnheader"] {
+        display: flex;
+
+        flex-wrap: wrap;
+        align-items: center;
+        justify-content: center;
+
+        height: 100%;
+
+        & > div {
+            flex: 1 100%;
+            height: auto;
+        }
+
+        & > div[ui-grid-filter] {
+            flex: auto;
+        }
+    }
+
+    .ui-grid-header-span {
+        position: relative;
+        border-right: 0;
+        background: #f5f5f5;
+
+        .ng-hide + .ui-grid-header-cell-row .ui-grid-header-cell {
+            height: 58px;
+        }
+
+        .ng-hide + .ui-grid-header-cell-row .ui-grid-cell-contents {
+            padding: 5px 5px;
+        }
+
+        .ui-grid-column-resizer.right {
+            top: -100px;
+        }
+
+        .ng-hide + .ui-grid-header-cell-row .ui-grid-column-resizer.right {
+            bottom: 0;
+        }
+
+        &.ui-grid-header-cell:not(:first-child) {
+            left: 0;
+            box-shadow: -1px 0px 0 0 #d4d4d4;
+        }
+
+        &.ui-grid-header-cell .ui-grid-header-cell .ui-grid-column-resizer.right {
+            border-right-width: 0;
+        }
+
+        &.ui-grid-header-cell .ui-grid-header-cell:last-child .ui-grid-column-resizer.right {
+            // Hide all right borders, and fix cell offset.
+            right: -1px;
+            border: none;
+        }
+
+        &.ui-grid-header-cell [ng-show] .ui-grid-cell-contents {
+            text-indent: -20px;
+            margin-right: -20px;
+        }
+
+        & > div > .ui-grid-cell-contents {
+            border-bottom: 1px solid $table-border-color;
+        }
+    }
+
+    input {
+        line-height: 21px;
+    }
+}
+
+.ui-grid[ui-grid-selection][ui-grid-grouping],
+.ui-grid[ui-grid-selection][ui-grid-tree-view] {
+    .ui-grid-pinned-container-left {
+        .ui-grid-header--subcategories {
+            .ui-grid-header-span {
+                &.ui-grid-header-cell {
+                    box-shadow: none;
+                }
+            }
+        }
+    }
+}
diff --git a/modules/frontend/app/primitives/ui-grid-header/index.tpl.pug b/modules/frontend/app/primitives/ui-grid-header/index.tpl.pug
new file mode 100644
index 0000000..8a41881
--- /dev/null
+++ b/modules/frontend/app/primitives/ui-grid-header/index.tpl.pug
@@ -0,0 +1,37 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.ui-grid-header.ui-grid-header--subcategories(role='rowgroup')
+    .ui-grid-top-panel
+        .ui-grid-header-viewport
+            .ui-grid-header-canvas
+                .ui-grid-header-cell-wrapper(ng-style='colContainer.headerCellWrapperStyle()')
+                    .ui-grid-header-cell-row(role='row')
+                        .ui-grid-header-cell.ui-grid-clearfix(
+                            ng-if='col.colDef.name === "treeBaseRowHeaderCol" || col.colDef.name === "selectionRowHeaderCol"'
+                            ng-repeat='col in colContainer.renderedColumns track by col.uid'
+                            ng-class='{ disabled: !grid.options.multiSelect && col.colDef.name === "selectionRowHeaderCol"}'
+
+                            col='col'
+                            ui-grid-header-cell=''
+                            render-index='$index'
+                        )
+                        .ui-grid-header-span.ui-grid-header-cell.ui-grid-clearfix.ui-grid-category(ng-repeat='cat in grid.options.categories', ng-if='cat.visible && \
+                        (colContainer.renderedColumns | uiGridSubcategories: cat.name).length > 0')
+                            div(ng-show='(colContainer.renderedColumns|uiGridSubcategories:cat.name).length > 1')
+                                .ui-grid-cell-contents {{ cat.name }}
+                            .ui-grid-header-cell-row
+                                .ui-grid-header-cell.ui-grid-clearfix(ng-repeat='col in (colContainer.renderedColumns|uiGridSubcategories:cat.name) track by col.uid' ui-grid-header-cell='' col='col' render-index='$index')
diff --git a/modules/frontend/app/primitives/ui-grid-settings/index.scss b/modules/frontend/app/primitives/ui-grid-settings/index.scss
new file mode 100644
index 0000000..5b67ff4
--- /dev/null
+++ b/modules/frontend/app/primitives/ui-grid-settings/index.scss
@@ -0,0 +1,272 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../../public/stylesheets/variables';
+
+.ui-grid-settings {
+    color: #393939;
+
+    .grid-settings {
+        ul.dropdown-menu {
+            padding: 0;
+            min-width: auto;
+
+            & > li {
+                & > a {
+                    padding: 3px 12px;
+
+                    & > span {
+                        line-height: 26px;
+                        padding-left: 10px;
+                        padding-right: 8px;
+                        cursor: pointer;
+                    }
+
+                    & > i {
+                        position: relative;
+                        
+                        width: 12px;
+                        height: 12px;
+                        margin-top: 7px;
+                        margin-left: 0;
+
+                        color: inherit;
+                        line-height: 26px;
+
+                        border: 1px solid #afafaf;
+                        border-radius: 2px;
+                        background-color: #FFF;
+
+                        box-shadow: inset 0 1px 1px #ccc;
+
+                        &.fa-square-o:before,
+                        &.fa-check-square-o:before {
+                            content: '';
+                        }
+
+                        &.fa-check-square-o {
+                            border-color: #0067b9;
+                            background-color: #0067b9;
+
+                            box-shadow: none;
+                        }
+
+                        &.fa-check-square-o:before {
+                            content: '';
+
+                            position: absolute;
+                            top: 0;
+                            left: 3px;
+
+                            width: 4px;
+                            height: 8px;
+
+                            border: solid #FFF;
+                            border-width: 0 2px 2px 0;
+
+                            transform: rotate(35deg);
+                        }
+                    }
+                }
+
+                &:not(:last-child) {
+                    border-bottom: 1px solid #dddddd;
+                }
+            }
+        }
+    }
+
+    &--heading {
+        cursor: default;
+
+        sub {
+            bottom: 0;
+            height: 12px;
+            margin-left: 22px;
+
+            font-size: 12px;
+            line-height: 1;
+            text-align: left;
+            color: $gray-light;
+        }
+    }
+
+    &--heading > span {
+        position: relative;
+        top: -1px;
+    }
+
+    &-filter {
+        .ignite-form-field {
+            $height: 36px;
+
+            width: 260px;
+
+            font-size: 14px;
+
+            label {
+                width: auto;
+                max-width: initial;
+                margin-right: 10px;
+
+                font-size: inherit;
+                line-height: $height;
+            }
+
+            &__control {
+                width: 190px;
+
+                input {
+                    &:focus {
+                        box-shadow: none;
+                    }
+                }
+            }
+        }
+    }
+
+    &-number-filter {
+        .ignite-form-field {
+            width: 180px;
+            margin-right: 0;
+
+            &__label {
+            }
+
+            &__control {
+            }
+
+            &:nth-child(1) {
+                float: left;
+
+                .ignite-form-field__label {
+                    margin-right: 0;
+                    width: 70%;
+                    max-width: 100%;
+                }
+
+                .ignite-form-field__control {
+                    width: 30%;
+                }
+            }
+        }
+
+        button {
+            width: auto;
+            display: inline-block;
+            margin-left: 5px;
+        }
+    }
+
+    &-dateperiod {
+        display: block;
+        margin-right: 35px;
+
+        .ignite-form-field {
+            $height: 36px;
+
+            display: inline-block;
+            width: auto;
+
+            font-size: 14px;
+
+            label.ignite-form-field__label {
+                width: auto;
+                max-width: initial;
+
+                font-size: inherit;
+                line-height: $height;
+            }
+
+            .ignite-form-field__control {
+                @import "./../../../public/stylesheets/variables.scss";
+
+                width: auto;
+
+                .input-tip {
+                    overflow: visible;
+
+                    & > button {
+                        overflow: visible;
+
+                        width: auto;
+                        height: $height;
+                        min-width: 70px;
+                        max-width: 70px;
+                        padding: 0 0 0 5px;
+
+                        cursor: pointer;
+                        color: transparent;
+                        font-size: inherit;
+                        line-height: $height;
+                        text-align: left;
+                        text-shadow: 0 0 0 $ignite-brand-success;
+
+                        border: none;
+                        box-shadow: none;
+
+                        &:hover, &:focus {
+                            text-shadow: 0 0 0 change-color($ignite-brand-success, $lightness: 26%);
+                        }
+
+                        button {
+                            color: $text-color;
+                        }
+                    }
+                }
+
+                ul.dropdown-menu {
+                    span {
+                        text-shadow: none;
+                    }
+                }
+            }
+        }
+    }
+
+    .btn-ignite,
+    &-filter,
+    &-number-filter,
+    &-dateperiod {
+        float: right;
+
+        margin-left: 20px;
+        margin-right: 0;
+
+        .timepicker--ignite {
+            width: 45px;
+        }
+    }
+
+    &-dateperiod {
+        margin-left: 15px;
+        margin-right: -5px;
+    }
+
+    .grid-settings {
+        svg {
+            cursor: pointer;
+            color: #424242;
+        }
+    }
+}
+
+.grid-settings {
+    display: inline-block;
+
+    margin-left: 10px;
+}
diff --git a/modules/frontend/app/primitives/ui-grid/index.scss b/modules/frontend/app/primitives/ui-grid/index.scss
new file mode 100644
index 0000000..49a16ae
--- /dev/null
+++ b/modules/frontend/app/primitives/ui-grid/index.scss
@@ -0,0 +1,554 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../../public/stylesheets/variables';
+
+// Use this class to control grid header height
+.ui-grid-ignite__panel {
+    $panel-height: 64px;
+    $title-height: 36px;
+    padding-top: ($panel-height - $title-height) / 2 !important;
+    padding-bottom: ($panel-height - $title-height) / 2 !important;
+}
+
+.ui-grid.ui-grid--ignite {
+    $height: 46px;
+
+    position: relative;
+    border-top: none;
+
+    [role="button"] {
+        outline: none;
+    }
+
+    sup, sub {
+        color: $ignite-brand-success;
+    }
+
+    .ui-grid-top-panel {
+        background: initial;
+    }
+
+    .ui-grid-canvas {
+        padding-top: 0;
+    }
+
+    .ui-grid-cell {
+        height: $height - 1px;
+
+        border-color: transparent;
+    }
+
+    .ui-grid-cell,
+    .ui-grid-header-cell {
+        .ui-grid-cell-contents {
+            padding: 13px 20px;
+
+            text-align: left;
+            white-space: nowrap;
+        }
+    }
+
+    .ui-grid-contents-wrapper {
+        position: absolute;
+        top: 0;
+
+        border-bottom-right-radius: 6px;
+        overflow: hidden;
+    }
+
+    .ui-grid-render-container-body {
+        .ui-grid-cell {
+            .ui-grid-cell-contents {
+                text-align: left;
+            }
+
+            &.ui-grid-number-cell {
+                .ui-grid-cell-contents {
+                    text-align: right;
+                }
+            }
+        }
+    }
+
+    .ui-grid-row:last-child .ui-grid-cell {
+        border-bottom-width: 0;
+    }
+
+    .ui-grid-header-viewport {
+        .ui-grid-header-canvas {
+            .ui-grid-header-cell {
+                .ui-grid-cell-contents {
+                    color: $gray-light;
+                    font-size: 14px;
+                    font-weight: normal;
+                    font-style: normal;
+                    line-height: 18px;
+                    text-align: left;
+
+                    padding: 15px 20px;
+
+                    & > i {
+                        line-height: 18px;
+                    }
+
+                    .ui-grid-header-cell-label {
+                        // position: relative;
+                    }
+
+                    .ui-grid-header-cell-label + span {
+                        position: relative;
+                        right: 3px;
+                    }
+
+                    .ui-grid-header-cell-filter {
+                        background-image:
+                            linear-gradient(to right, $ignite-brand-success, transparent),
+                            linear-gradient(to right, $ignite-brand-success 70%, transparent 0%);
+                        background-position: left bottom;
+                        background-repeat: repeat-x;
+                        background-size: 0, 8px 1px, 0, 0;
+
+                        &:hover {
+                            background-image: none;
+                            // linear-gradient(to right, change-color($ignite-brand-success, $lightness: 26%), transparent),
+                            // linear-gradient(to right, change-color($ignite-brand-success, $lightness: 26%) 70%, transparent 0%);
+                        }
+
+                        div {
+                            z-index: 1;
+                            position: fixed;
+
+                            width: 100px;
+                            height: 20px;
+                            margin-top: -20px;
+
+                            font-size: 0;
+                        }
+
+                        &.active {
+                            color: $ignite-brand-primary;
+
+                            background-image:
+                                linear-gradient(to right, $ignite-brand-primary, transparent),
+                                linear-gradient(to right, $ignite-brand-primary 70%, transparent 0%);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    .ui-grid-header--subcategories {
+        .ui-grid-header-canvas {
+            background-color: white;
+        }
+
+        .ui-grid-header-span.ui-grid-header-cell {
+            background: initial;
+
+            .ui-grid-cell-contents {
+                padding: 8px 20px;
+            }
+
+            [ng-show] .ui-grid-cell-contents {
+                text-align: center;
+            }
+
+            .ui-grid-filter-container {
+                padding-left: 20px;
+                padding-right: 20px;
+                font-weight: normal;
+            }
+
+            .ng-hide + .ui-grid-header-cell-row .ui-grid-header-cell {
+                height: 69px;
+            }
+
+            .ng-hide + .ui-grid-header-cell-row {
+                .ui-grid-cell-contents {
+                    padding: 8px 20px;
+                }
+
+                .ui-grid-filter-container {
+                    padding-left: 20px;
+                    padding-right: 20px;
+                }
+            }
+        }
+    }
+
+    .ui-grid-pinned-container {
+        &.ui-grid-pinned-container-left {
+            width: auto;
+
+            .ui-grid-render-container-left {
+                .ui-grid-viewport,
+                .ui-grid-header-viewport {
+                    width: auto;
+
+                    .ui-grid-canvas {
+                        width: auto;
+                    }
+                }
+
+                .ui-grid-header--subcategories {
+                    .ui-grid-selection-row-header-buttons {
+                        margin-top: 12px;
+
+                        &:after {
+                            top: 3px;
+                        }
+                    }
+                }
+
+                .ui-grid-cell.ui-grid-disable-selection.ui-grid-row-header-cell {
+                    pointer-events: auto;
+                }
+
+                &:before {
+                    content: '';
+
+                    position: absolute;
+                    top: 0;
+                    right: 15px;
+                    z-index: 1000;
+
+                    width: 5px;
+                    height: 100%;
+
+                    opacity: .2;
+                    box-shadow: 2px 0 3px #000;
+                    border-right: 1px solid #000;
+                }
+            }
+        }
+    }
+
+    .ui-grid-pinned-container-left .ui-grid-header-cell:last-child {
+        border-width: 0;
+    }
+
+    .ui-grid-pinned-container-left .ui-grid-cell:last-child {
+        border-width: 0;
+        background-color: initial;
+    }
+
+    .ui-grid-row {
+        height: $height;
+        border-bottom: 1px solid $table-border-color;
+
+        &:nth-child(odd) {
+            .ui-grid-cell {
+                background-color: initial;
+            }
+        }
+
+        &:nth-child(even) {
+            .ui-grid-cell {
+                background-color: #f9f9f9;
+            }
+        }
+
+        &.ui-grid-row-selected > [ui-grid-row] > .ui-grid-cell {
+            background-color: #e5f2f9;
+
+            box-shadow: 0 -1px 0 0 #c6cfd8, 0 1px 0 0 #c6cfd8;
+        }
+    }
+
+    .ui-grid-selection-row-header-buttons {
+        position: relative;
+        opacity: 1;
+        right: 3px;
+        display: block;
+
+        &::before {
+            content: '';
+
+            width: 12px;
+            height: 12px;
+
+            margin-left: 0;
+            margin-right: 0;
+
+            border: 1px solid #afafaf;
+            border-radius: 2px;
+            background-color: #FFF;
+
+            box-shadow: inset 0 1px 1px #ccc;
+        }
+
+        &.ui-grid-all-selected,
+        &.ui-grid-row-selected {
+
+            &::before {
+                border-color: #0067b9;
+                background-color: #0067b9;
+
+                box-shadow: none;
+            }
+
+            &::after {
+                content: '';
+
+                position: absolute;
+                top: 4px;
+                left: 4px;
+
+                width: 4px;
+                height: 8px;
+
+                border: solid #FFF;
+                border-width: 0 2px 2px 0;
+
+                transform: rotate(35deg);
+            }
+        }
+    }
+
+    .ui-grid-header,
+    .ui-grid-viewport {
+        .ui-grid-icon-cancel {
+            right: 10px;
+        }
+
+        .ui-grid-tree-base-row-header-buttons {
+            .ui-grid-icon-plus-squared,
+            .ui-grid-icon-minus-squared,
+            &.ui-grid-icon-plus-squared,
+            &.ui-grid-icon-minus-squared {
+                position: relative;
+                top: 3px;
+
+                display: block;
+                width: 13px;
+                height: 13px;
+
+                margin-top: -1px;
+                margin-left: -4px;
+                margin-right: 0;
+
+                cursor: pointer;
+
+                border: 1px solid #757575;
+                border-radius: 2px;
+                background-color: #757575;
+
+                &::before,
+                &::after {
+                    content: '';
+                }
+            }
+
+            .ui-grid-icon-plus-squared,
+            .ui-grid-icon-minus-squared,
+            &.ui-grid-icon-plus-squared,
+            &.ui-grid-icon-minus-squared {
+                &::before {
+                    position: absolute;
+                    top: 5px;
+                    left: 2px;
+
+                    width: 7px;
+                    margin: 0;
+
+                    border-top: 1px solid white;
+                }
+            }
+
+            .ui-grid-icon-plus-squared,
+            &.ui-grid-icon-plus-squared {
+                &::after {
+                    position: absolute;
+                    top: 2px;
+                    left: 5px;
+
+                    height: 7px;
+                    margin: 0;
+
+                    border-left: 1px solid white;
+                }
+            }
+        }
+    }
+
+    .ui-grid-header--subcategories {
+        .ui-grid-icon-cancel {
+            right: 20px;
+        }
+    }
+
+    .ui-grid-pinned-container {
+        .ui-grid-header {
+            .ui-grid-header-cell-row {
+                .ui-grid-header-cell {
+                    border-right: none;
+
+                    &.disabled {
+                        opacity: .2;
+
+                        .ui-grid-icon-ok {
+                            cursor: default;
+                        }
+                    }
+
+                    &:last-child {
+                        .ui-grid-header-cell {
+                            .ui-grid-column-resizer {
+                                right: -1px;
+                                opacity: 0;
+                                z-index: 1000;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        .ui-grid-viewport {
+            .ui-grid-row {
+                .ui-grid-cell {
+                    border-bottom: none;
+
+                    &:nth-child(2) {
+                        overflow: visible;
+                    }
+                }
+            }
+        }
+
+        .ui-grid-tree-header-row {
+            & ~ .ui-grid-row:not(.ui-grid-tree-header-row) {
+                position: relative;
+
+                &::before {
+                    content: '';
+                    position: absolute;
+                    top: 0;
+                    left: 0;
+                    z-index: 1;
+
+                    width: 4px;
+                    height: 47px;
+
+                    background: #0067b9;
+                    box-shadow: 0 -1px 0 0 rgba(0, 0, 0, .3), 0 -1px 0 0 rgba(0, 103, 185, 1);
+                }
+            }
+        }
+    }
+
+    .ui-grid-tree-header-row {
+        font-weight: normal !important;
+    }
+
+    input[type="text"].ui-grid-filter-input {
+        display: block;
+        width: 100%;
+        height: 28px;
+        padding: 3px 3px;
+
+        border: 1px solid #ccc;
+        border-radius: 4px;
+        background-color: #fff;
+        background-image: none;
+
+        color: #393939;
+        text-align: left;
+        font-size: 14px;
+        font-weight: normal;
+        line-height: 1.42857;
+
+        box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+        transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+
+        &::placeholder {
+            color: #999;
+        }
+
+        &:focus {
+            outline: none;
+
+            box-shadow: none;
+            border-color: #66afe9;
+        }
+    }
+
+    .ui-grid-icon-cancel {
+        &:before {
+            content: '';
+
+            display: block;
+            width: 12px;
+            height: 12px;
+            margin: 10px 5px;
+
+            background-image: url('/images/icons/cross.svg');
+            background-repeat: no-repeat;
+            background-position: center;
+        }
+    }
+
+    .ui-grid-icon-filter {
+        position: absolute;
+        right: 20px;
+
+        color: $text-color;
+
+        &:before {
+            content: '';
+        }
+    }
+
+    .ui-grid-selection-row-header-buttons::before {
+        opacity: 1;
+    }
+
+    .ui-grid-clearfix:before, .ui-grid-clearfix:after {
+        display: flex;
+    }
+}
+
+.ui-grid--ignite.ui-grid-disabled-group-selection {
+    .ui-grid-pinned-container {
+        .ui-grid-tree-header-row {
+            .ui-grid-selection-row-header-buttons {
+                opacity: .2;
+                cursor: default;
+            }
+        }
+    }
+}
+
+// Obsoleted, use grid-no-data.
+.ui-grid--ignite.no-data {
+    position: relative;
+
+    padding: 16px 51px;
+
+    border-radius: 0 0 4px 4px;
+
+    font-style: italic;
+    line-height: 16px;
+}
+
+.ui-grid {
+    input[type="text"].ui-grid-filter-input {
+        font-weight: normal;
+    }
+}
diff --git a/modules/frontend/app/services/AngularStrapSelect.decorator.js b/modules/frontend/app/services/AngularStrapSelect.decorator.js
new file mode 100644
index 0000000..44ed8ed
--- /dev/null
+++ b/modules/frontend/app/services/AngularStrapSelect.decorator.js
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import _ from 'lodash';
+
+/**
+ * Special decorator that fix problem in AngularStrap selectAll / deselectAll methods.
+ * If this problem will be fixed in AngularStrap we can remove this delegate.
+ */
+export default angular.module('mgcrea.ngStrap.select')
+    .decorator('$select', ['$delegate', function($delegate) {
+        function SelectFactoryDecorated(element, controller, config) {
+            const delegate = $delegate(element, controller, config);
+
+            // Common vars.
+            const options = Object.assign({}, $delegate.defaults, config);
+
+            const scope = delegate.$scope;
+
+            const valueByIndex = (index) => {
+                if (_.isUndefined(scope.$matches[index]))
+                    return null;
+
+                return scope.$matches[index].value;
+            };
+
+            const selectAll = (active) => {
+                const selected = [];
+
+                scope.$apply(() => {
+                    for (let i = 0; i < scope.$matches.length; i++) {
+                        if (scope.$isActive(i) === active) {
+                            selected[i] = scope.$matches[i].value;
+
+                            delegate.activate(i);
+
+                            controller.$setViewValue(scope.$activeIndex.map(valueByIndex));
+                        }
+                    }
+                });
+
+                // Emit events.
+                for (let i = 0; i < selected.length; i++) {
+                    if (selected[i])
+                        scope.$emit(options.prefixEvent + '.select', selected[i], i, delegate);
+                }
+            };
+
+            scope.$selectAll = () => {
+                scope.$$postDigest(selectAll.bind(this, false));
+            };
+
+            scope.$selectNone = () => {
+                scope.$$postDigest(selectAll.bind(this, true));
+            };
+
+            return delegate;
+        }
+
+        SelectFactoryDecorated.defaults = $delegate.defaults;
+
+        return SelectFactoryDecorated;
+    }]);
diff --git a/modules/frontend/app/services/AngularStrapTooltip.decorator.js b/modules/frontend/app/services/AngularStrapTooltip.decorator.js
new file mode 100644
index 0000000..f1f8673
--- /dev/null
+++ b/modules/frontend/app/services/AngularStrapTooltip.decorator.js
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import _ from 'lodash';
+
+/**
+ * Decorator that fix problem in AngularStrap $tooltip.
+ */
+export default angular
+    .module('mgcrea.ngStrap.tooltip')
+    /**
+     * Don't hide tooltip when mouse move from element to tooltip.
+     */
+    .decorator('$tooltip', ['$delegate', function($delegate) {
+        function TooltipFactoryDecorated(element, config) {
+            let tipElementEntered = false;
+
+            config.onShow = ($tooltip) => {
+                // Workaround for tooltip detection.
+                if ($tooltip.$element && $tooltip.$options.trigger === 'click hover') {
+                    $tooltip.$element.on('mouseenter', () => tipElementEntered = true);
+                    $tooltip.$element.on('mouseleave', () => {
+                        tipElementEntered = false;
+
+                        $tooltip.leave();
+                    });
+                }
+            };
+
+            const $tooltip = $delegate(element, config);
+
+            const scope = $tooltip.$scope;
+            const options = $tooltip.$options;
+
+            const _hide = $tooltip.hide;
+
+            $tooltip.hide = (blur) => {
+                if (!$tooltip.$isShown || tipElementEntered)
+                    return;
+
+                if ($tooltip.$element) {
+                    $tooltip.$element.off('mouseenter');
+                    $tooltip.$element.off('mouseleave');
+
+                    return _hide(blur);
+                }
+
+                scope.$emit(options.prefixEvent + '.hide.before', $tooltip);
+
+                if (!_.isUndefined(options.onBeforeHide) && _.isFunction(options.onBeforeHide))
+                    options.onBeforeHide($tooltip);
+
+                $tooltip.$isShown = scope.$isShown = false;
+                scope.$$phase || (scope.$root && scope.$root.$$phase) || scope.$digest();
+            };
+
+            return $tooltip;
+        }
+
+        return TooltipFactoryDecorated;
+    }])
+    /**
+     * Set width for dropdown as for element.
+     */
+    .decorator('$tooltip', ['$delegate', ($delegate) => {
+        return function(el, config) {
+            const $tooltip = $delegate(el, config);
+
+            $tooltip.$referenceElement = el;
+            $tooltip.destroy = _.flow($tooltip.destroy, () => $tooltip.$referenceElement = null);
+            $tooltip.$applyPlacement = _.flow($tooltip.$applyPlacement, () => {
+                if (!$tooltip.$element)
+                    return;
+
+                const refWidth = $tooltip.$referenceElement[0].getBoundingClientRect().width;
+                const elWidth = $tooltip.$element[0].getBoundingClientRect().width;
+
+                if (refWidth > elWidth) {
+                    $tooltip.$element.css({
+                        width: refWidth,
+                        maxWidth: 'initial'
+                    });
+                }
+            });
+
+            return $tooltip;
+        };
+    }]);
diff --git a/modules/frontend/app/services/CSV.js b/modules/frontend/app/services/CSV.js
new file mode 100644
index 0000000..ac87bbf
--- /dev/null
+++ b/modules/frontend/app/services/CSV.js
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class CSV {
+    getSeparator() {
+        return (0.5).toLocaleString().includes(',') ? ';' : ',';
+    }
+}
diff --git a/modules/frontend/app/services/ChartColors.service.js b/modules/frontend/app/services/ChartColors.service.js
new file mode 100644
index 0000000..061ff3f
--- /dev/null
+++ b/modules/frontend/app/services/ChartColors.service.js
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import COLORS from 'app/data/colors.json';
+
+export default function() {
+    return COLORS;
+}
diff --git a/modules/frontend/app/services/Confirm.service.js b/modules/frontend/app/services/Confirm.service.js
new file mode 100644
index 0000000..953bdcd
--- /dev/null
+++ b/modules/frontend/app/services/Confirm.service.js
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import templateUrl from 'views/templates/confirm.tpl.pug';
+import {CancellationError} from 'app/errors/CancellationError';
+
+export class Confirm {
+    static $inject = ['$modal', '$q'];
+    /**
+     * @param {mgcrea.ngStrap.modal.IModalService} $modal
+     * @param {ng.IQService} $q
+     */
+    constructor($modal, $q) {
+        this.$modal = $modal;
+        this.$q = $q;
+    }
+    /**
+     * @param {string} content - Confirmation text/html content
+     * @param {boolean} yesNo - Show "Yes/No" buttons instead of "Config"
+     * @return {ng.IPromise}
+     */
+    confirm(content = 'Confirm?', yesNo = false) {
+        return this.$q((resolve, reject) => {
+            this.$modal({
+                templateUrl,
+                backdrop: true,
+                onBeforeHide: () => reject(new CancellationError()),
+                controller: ['$scope', function($scope) {
+                    $scope.yesNo = yesNo;
+                    $scope.content = content;
+                    $scope.confirmCancel = $scope.confirmNo = () => {
+                        reject(new CancellationError());
+                        $scope.$hide();
+                    };
+                    $scope.confirmYes = () => {
+                        resolve();
+                        $scope.$hide();
+                    };
+                }]
+            });
+        });
+    }
+}
+
+/**
+ * Confirm popup service.
+ * @deprecated Use Confirm instead
+ * @param {ng.IRootScopeService} $root
+ * @param {ng.IQService} $q
+ * @param {mgcrea.ngStrap.modal.IModalService} $modal
+ * @param {ng.animate.IAnimateService} $animate
+ */
+export default function IgniteConfirm($root, $q, $modal, $animate) {
+    const scope = $root.$new();
+
+    const modal = $modal({templateUrl, scope, show: false, backdrop: true});
+
+    const _hide = () => {
+        $animate.enabled(modal.$element, false);
+
+        modal.hide();
+    };
+
+    let deferred;
+
+    scope.confirmYes = () => {
+        _hide();
+
+        deferred.resolve(true);
+    };
+
+    scope.confirmNo = () => {
+        _hide();
+
+        deferred.resolve(false);
+    };
+
+    scope.confirmCancel = () => {
+        _hide();
+
+        deferred.reject(new CancellationError());
+    };
+
+    /**
+     *
+     * @param {String } content
+     * @param {Boolean} [yesNo]
+     * @returns {Promise}
+     */
+    modal.confirm = (content, yesNo) => {
+        scope.content = content || 'Confirm?';
+        scope.yesNo = !!yesNo;
+
+        deferred = $q.defer();
+
+        modal.$promise.then(modal.show);
+
+        return deferred.promise;
+    };
+
+    return modal;
+}
+
+IgniteConfirm.$inject = ['$rootScope', '$q', '$modal', '$animate'];
diff --git a/modules/frontend/app/services/ConfirmBatch.service.js b/modules/frontend/app/services/ConfirmBatch.service.js
new file mode 100644
index 0000000..5c6961c
--- /dev/null
+++ b/modules/frontend/app/services/ConfirmBatch.service.js
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import templateUrl from 'views/templates/batch-confirm.tpl.pug';
+import {CancellationError} from 'app/errors/CancellationError';
+
+// Service for confirm or skip several steps.
+export default class IgniteConfirmBatch {
+    static $inject = ['$rootScope', '$q', '$modal'];
+
+    /**
+     * @param {ng.IRootScopeService} $root 
+     * @param {ng.IQService} $q
+     * @param {mgcrea.ngStrap.modal.IModalService} $modal
+     */
+    constructor($root, $q, $modal) {
+        const scope = $root.$new();
+
+        scope.confirmModal = $modal({
+            templateUrl,
+            scope,
+            show: false,
+            backdrop: 'static',
+            keyboard: false
+        });
+
+        const _done = (cancel) => {
+            scope.confirmModal.hide();
+
+            if (cancel)
+                scope.deferred.reject(new CancellationError());
+            else
+                scope.deferred.resolve();
+        };
+
+        const _nextElement = (skip) => {
+            scope.items[scope.curIx++].skip = skip;
+
+            if (scope.curIx < scope.items.length)
+                scope.content = scope.contentGenerator(scope.items[scope.curIx]);
+            else
+                _done();
+        };
+
+        scope.cancel = () => {
+            _done(true);
+        };
+
+        scope.skip = (applyToAll) => {
+            if (applyToAll) {
+                for (let i = scope.curIx; i < scope.items.length; i++)
+                    scope.items[i].skip = true;
+
+                _done();
+            }
+            else
+                _nextElement(true);
+        };
+
+        scope.overwrite = (applyToAll) => {
+            if (applyToAll)
+                _done();
+            else
+                _nextElement(false);
+        };
+
+        /**
+         * Show confirm all dialog.
+         * @template T
+         * @param {(T) => string} confirmMessageFn Function to generate a confirm message.
+         * @param {Array<T>} [itemsToConfirm] Array of element to process by confirm.
+         */
+        this.confirm = function confirm(confirmMessageFn, itemsToConfirm) {
+            scope.deferred = $q.defer();
+
+            scope.contentGenerator = confirmMessageFn;
+
+            scope.items = itemsToConfirm;
+            scope.curIx = 0;
+            scope.content = (scope.items && scope.items.length > 0) ? scope.contentGenerator(scope.items[0]) : null;
+
+            scope.confirmModal.$promise.then(scope.confirmModal.show);
+
+            return scope.deferred.promise;
+        };
+    }
+}
diff --git a/modules/frontend/app/services/CopyToClipboard.service.js b/modules/frontend/app/services/CopyToClipboard.service.js
new file mode 100644
index 0000000..f3c6309
--- /dev/null
+++ b/modules/frontend/app/services/CopyToClipboard.service.js
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+/**
+ * Service to copy some value to OS clipboard.
+ * @param {ng.IWindowService} $window
+ * @param {ReturnType<typeof import('./Messages.service').default>} Messages
+ */
+export default function factory($window, Messages) {
+    const body = angular.element($window.document.body);
+
+    /** @type {JQuery<HTMLTextAreaElement>} */
+    const textArea = angular.element('<textarea/>');
+
+    textArea.css({
+        position: 'fixed',
+        opacity: '0'
+    });
+
+    return {
+        /**
+         * @param {string} toCopy
+         */
+        copy(toCopy) {
+            textArea.val(toCopy);
+
+            body.append(textArea);
+
+            textArea[0].select();
+
+            try {
+                if (document.execCommand('copy'))
+                    Messages.showInfo('Value copied to clipboard');
+                else
+                    window.prompt('Copy to clipboard: Ctrl+C, Enter', toCopy); // eslint-disable-line no-alert
+            }
+            catch (err) {
+                window.prompt('Copy to clipboard: Ctrl+C, Enter', toCopy); // eslint-disable-line no-alert
+            }
+
+            textArea.remove();
+        }
+    };
+}
+
+factory.$inject = ['$window', 'IgniteMessages'];
diff --git a/modules/frontend/app/services/Countries.service.js b/modules/frontend/app/services/Countries.service.js
new file mode 100644
index 0000000..2de6716
--- /dev/null
+++ b/modules/frontend/app/services/Countries.service.js
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+import COUNTRIES from 'app/data/countries.json';
+
+/**
+ * @typedef {{label:string,value:string,code:string}} Country
+ */
+
+export default function() {
+    const indexByName = _.keyBy(COUNTRIES, 'label');
+    const UNDEFINED_COUNTRY = {label: '', value: '', code: ''};
+
+    /**
+     * @param {string} name
+     * @return {Country}
+     */
+    const getByName = (name) => (indexByName[name] || UNDEFINED_COUNTRY);
+    /**
+     * @returns {Array<Country>}
+     */
+    const getAll = () => (COUNTRIES);
+
+    return {
+        getByName,
+        getAll
+    };
+}
diff --git a/modules/frontend/app/services/DefaultState.js b/modules/frontend/app/services/DefaultState.js
new file mode 100644
index 0000000..b46bfec
--- /dev/null
+++ b/modules/frontend/app/services/DefaultState.js
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @param {import('@uirouter/angularjs').StateProvider} $stateProvider
+ */
+function DefaultState($stateProvider) {
+    const stateName = 'default-state';
+
+    $stateProvider.state(stateName, {});
+
+    return {
+        setRedirectTo(fn) {
+            const state = $stateProvider.stateRegistry.get(stateName);
+            state.redirectTo = fn(state.redirectTo);
+        },
+        $get() {
+            return this;
+        }
+    };
+}
+
+DefaultState.$inject = ['$stateProvider'];
+
+export default DefaultState;
diff --git a/modules/frontend/app/services/ErrorParser.service.js b/modules/frontend/app/services/ErrorParser.service.js
new file mode 100644
index 0000000..88642b9
--- /dev/null
+++ b/modules/frontend/app/services/ErrorParser.service.js
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import isEmpty from 'lodash/isEmpty';
+import {nonEmpty} from 'app/utils/lodashMixins';
+
+export default class {
+    static $inject = ['JavaTypes'];
+
+    /**
+     * @param {import('./JavaTypes.service').default} JavaTypes
+     */
+    constructor(JavaTypes) {
+        this.JavaTypes = JavaTypes;
+    }
+
+    extractMessage(err, prefix) {
+        prefix = prefix || '';
+
+        if (err) {
+            if (err.hasOwnProperty('data'))
+                err = err.data;
+
+            if (err.hasOwnProperty('message')) {
+                let msg = err.message;
+
+                const traceIndex = msg.indexOf(', trace=');
+
+                if (traceIndex > 0)
+                    msg = msg.substring(0, traceIndex);
+
+                const lastIdx = msg.lastIndexOf(' err=');
+                let msgEndIdx = msg.indexOf(']', lastIdx);
+
+                if (lastIdx > 0 && msgEndIdx > 0) {
+                    let startIdx = msg.indexOf('[', lastIdx);
+
+                    while (startIdx > 0) {
+                        const tmpIdx = msg.indexOf(']', msgEndIdx + 1);
+
+                        if (tmpIdx > 0)
+                            msgEndIdx = tmpIdx;
+
+                        startIdx = msg.indexOf('[', startIdx + 1);
+                    }
+                }
+
+                return prefix + (lastIdx >= 0 ? msg.substring(lastIdx + 5, msgEndIdx > 0 ? msgEndIdx : traceIndex) : msg);
+            }
+
+            if (nonEmpty(err.className)) {
+                if (isEmpty(prefix))
+                    prefix = 'Internal cluster error: ';
+
+                return prefix + err.className;
+            }
+
+            return prefix + err;
+        }
+
+        return prefix + 'Internal error.';
+    }
+
+    extractFullMessage(err) {
+        const clsName = _.isEmpty(err.className) ? '' : '[' + this.JavaTypes.shortClassName(err.className) + '] ';
+
+        let msg = err.message || '';
+        const traceIndex = msg.indexOf(', trace=');
+
+        if (traceIndex > 0)
+            msg = msg.substring(0, traceIndex);
+
+        return clsName + (msg);
+    }
+}
diff --git a/modules/frontend/app/services/ErrorPopover.service.js b/modules/frontend/app/services/ErrorPopover.service.js
new file mode 100644
index 0000000..3b02a7e
--- /dev/null
+++ b/modules/frontend/app/services/ErrorPopover.service.js
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Service to show/hide error popover.
+ */
+export default class ErrorPopover {
+    static $inject = ['$popover', '$anchorScroll', '$timeout', 'IgniteFormUtils'];
+
+    /**
+     * @param {mgcrea.ngStrap.popover.IPopoverService} $popover
+     * @param {ng.IAnchorScrollService} $anchorScroll
+     * @param {ng.ITimeoutService} $timeout
+     * @param {ReturnType<typeof import('app/services/FormUtils.service').default>} FormUtils
+     */
+    constructor($popover, $anchorScroll, $timeout, FormUtils) {
+        this.$popover = $popover;
+        this.$anchorScroll = $anchorScroll;
+        this.$timeout = $timeout;
+        this.FormUtils = FormUtils;
+
+        this.$anchorScroll.yOffset = 55;
+
+        this._popover = null;
+    }
+
+    /**
+     * Check that element is document area.
+     *
+     * @param {HTMLElement} el Element to check.
+     * @returns {boolean} True when element in document area.
+     */
+    static _isElementInViewport(el) {
+        const rect = el.getBoundingClientRect();
+
+        return (
+            rect.top >= 0 &&
+            rect.left >= 0 &&
+            rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
+            rect.right <= (window.innerWidth || document.documentElement.clientWidth)
+        );
+    }
+
+    /**
+     * Internal show popover message with detected properties.
+     *
+     * @param {string }id Id element to show popover message.
+     * @param {string} message Message to show.
+     * @param showTime Time before popover will be hidden.
+     */
+    _show(id, message, showTime = 5000) {
+        const body = $('body');
+
+        let el = body.find('#' + id);
+
+        if (!el || el.length === 0)
+            el = body.find('[name="' + id + '"]');
+
+        if (el && el.length > 0) {
+            if (!ErrorPopover._isElementInViewport(el[0]))
+                el[0].scrollIntoView();
+
+
+            const newPopover = this.$popover(el, {content: message});
+
+            this._popover = newPopover;
+
+            this.$timeout(() => newPopover.$promise.then(() => {
+                newPopover.show();
+
+                // Workaround to fix popover location when content is longer than content template.
+                // https://github.com/mgcrea/angular-strap/issues/1497
+                this.$timeout(newPopover.$applyPlacement);
+            }), 400);
+            this.$timeout(() => newPopover.hide(), showTime);
+        }
+    }
+
+    /**
+     * Show popover message.
+     *
+     * @param {String} id ID of element to show popover.
+     * @param {String} message Message to show.
+     * @param {Object} [ui] Form UI object. When specified extend section with that name.
+     * @param {String} [panelId] ID of element owner panel. When specified focus element with that ID.
+     * @param {Number} [showTime] Time before popover will be hidden. 5 sec when not specified.
+     * @returns {boolean} False always.
+     */
+    show(id, message, ui, panelId, showTime) {
+        if (this._popover)
+            this._popover.hide();
+
+        if (ui && ui.isPanelLoaded) {
+            this.FormUtils.ensureActivePanel(ui, panelId, id);
+
+            this.$timeout(() => this._show(id, message, showTime), ui.isPanelLoaded(panelId) ? 200 : 500);
+        }
+        else
+            this._show(id, message);
+
+        return false;
+    }
+
+    /**
+     * Hide popover message.
+     */
+    hide() {
+        if (this._popover)
+            this._popover.hide();
+    }
+}
diff --git a/modules/frontend/app/services/Focus.service.js b/modules/frontend/app/services/Focus.service.js
new file mode 100644
index 0000000..a152ff1
--- /dev/null
+++ b/modules/frontend/app/services/Focus.service.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Service to transfer focus for specified element.
+ * @param {ng.ITimeoutService} $timeout
+ */
+export default function factory($timeout) {
+    return {
+        /**
+         * @param {string} id Element id
+         */
+        move(id) {
+            // Timeout makes sure that is invoked after any other event has been triggered.
+            // E.g. click events that need to run before the focus or inputs elements that are
+            // in a disabled state but are enabled when those events are triggered.
+            $timeout(() => {
+                const elem = $('#' + id);
+
+                if (elem.length > 0)
+                    elem[0].focus();
+            }, 100);
+        }
+    };
+}
+
+factory.$inject = ['$timeout'];
diff --git a/modules/frontend/app/services/FormUtils.service.js b/modules/frontend/app/services/FormUtils.service.js
new file mode 100644
index 0000000..aa46381
--- /dev/null
+++ b/modules/frontend/app/services/FormUtils.service.js
@@ -0,0 +1,464 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import _ from 'lodash';
+
+/**
+ * @param {ng.IWindowService} $window
+ * @param {ReturnType<typeof import('./Focus.service').default>} Focus
+ * @param {ng.IRootScopeService} $rootScope
+ */
+export default function service($window, Focus, $rootScope) {
+    function ensureActivePanel(ui, pnl, focusId) {
+        if (ui && ui.loadPanel) {
+            const collapses = $('[bs-collapse-target]');
+
+            ui.loadPanel(pnl);
+
+            const idx = _.findIndex(collapses, function(collapse) {
+                return collapse.id === pnl;
+            });
+
+            if (idx >= 0) {
+                const activePanels = ui.activePanels;
+
+                if (!_.includes(ui.topPanels, idx)) {
+                    ui.expanded = true;
+
+                    const customExpanded = ui[pnl];
+
+                    if (customExpanded)
+                        ui[customExpanded] = true;
+                }
+
+                if (!activePanels || activePanels.length < 1)
+                    ui.activePanels = [idx];
+                else if (!_.includes(activePanels, idx)) {
+                    const newActivePanels = _.cloneDeep(activePanels);
+
+                    newActivePanels.push(idx);
+
+                    ui.activePanels = newActivePanels;
+                }
+            }
+
+            if (!_.isNil(focusId))
+                Focus.move(focusId);
+        }
+    }
+
+    /** @type {CanvasRenderingContext2D} */
+    let context = null;
+
+    /**
+     * Calculate width of specified text in body's font.
+     *
+     * @param {string} text Text to calculate width.
+     * @returns {Number} Width of text in pixels.
+     */
+    function measureText(text) {
+        if (!context) {
+            const canvas = document.createElement('canvas');
+
+            context = canvas.getContext('2d');
+
+            const style = window.getComputedStyle(document.getElementsByTagName('body')[0]);
+
+            context.font = style.fontSize + ' ' + style.fontFamily;
+        }
+
+        return context.measureText(text).width;
+    }
+
+    /**
+     * Compact java full class name by max number of characters.
+     *
+     * @param names Array of class names to compact.
+     * @param nameLength Max available width in characters for simple name.
+     * @returns {*} Array of compacted class names.
+     */
+    function compactByMaxCharts(names, nameLength) {
+        for (let nameIx = 0; nameIx < names.length; nameIx++) {
+            const s = names[nameIx];
+
+            if (s.length > nameLength) {
+                let totalLength = s.length;
+
+                const packages = s.split('.');
+
+                const packageCnt = packages.length - 1;
+
+                for (let i = 0; i < packageCnt && totalLength > nameLength; i++) {
+                    if (packages[i].length > 0) {
+                        totalLength -= packages[i].length - 1;
+
+                        packages[i] = packages[i][0];
+                    }
+                }
+
+                if (totalLength > nameLength) {
+                    const className = packages[packageCnt];
+
+                    const classNameLen = className.length;
+
+                    let remains = Math.min(nameLength - totalLength + classNameLen, classNameLen);
+
+                    if (remains < 3)
+                        remains = Math.min(3, classNameLen);
+
+                    packages[packageCnt] = className.substring(0, remains) + '...';
+                }
+
+                let result = packages[0];
+
+                for (let i = 1; i < packages.length; i++)
+                    result += '.' + packages[i];
+
+                names[nameIx] = result;
+            }
+        }
+
+        return names;
+    }
+
+    /**
+     * Compact java full class name by max number of pixels.
+     *
+     * @param names Array of class names to compact.
+     * @param nameLength Max available width in characters for simple name. Used for calculation optimization.
+     * @param nameWidth Maximum available width in pixels for simple name.
+     * @returns {*} Array of compacted class names.
+     */
+    function compactByMaxPixels(names, nameLength, nameWidth) {
+        if (nameWidth <= 0)
+            return names;
+
+        const fitted = [];
+
+        const widthByName = [];
+
+        const len = names.length;
+
+        let divideTo = len;
+
+        for (let nameIx = 0; nameIx < len; nameIx++) {
+            fitted[nameIx] = false;
+
+            widthByName[nameIx] = nameWidth;
+        }
+
+        // Try to distribute space from short class names to long class names.
+        let remains = 0;
+
+        do {
+            for (let nameIx = 0; nameIx < len; nameIx++) {
+                if (!fitted[nameIx]) {
+                    const curNameWidth = measureText(names[nameIx]);
+
+                    if (widthByName[nameIx] > curNameWidth) {
+                        fitted[nameIx] = true;
+
+                        remains += widthByName[nameIx] - curNameWidth;
+
+                        divideTo -= 1;
+
+                        widthByName[nameIx] = curNameWidth;
+                    }
+                }
+            }
+
+            const remainsByName = remains / divideTo;
+
+            for (let nameIx = 0; nameIx < len; nameIx++) {
+                if (!fitted[nameIx])
+                    widthByName[nameIx] += remainsByName;
+            }
+        }
+        while (remains > 0);
+
+        // Compact class names to available for each space.
+        for (let nameIx = 0; nameIx < len; nameIx++) {
+            const s = names[nameIx];
+
+            if (s.length > (nameLength / 2 | 0)) {
+                let totalWidth = measureText(s);
+
+                if (totalWidth > widthByName[nameIx]) {
+                    const packages = s.split('.');
+
+                    const packageCnt = packages.length - 1;
+
+                    for (let i = 0; i < packageCnt && totalWidth > widthByName[nameIx]; i++) {
+                        if (packages[i].length > 1) {
+                            totalWidth -= measureText(packages[i].substring(1, packages[i].length));
+
+                            packages[i] = packages[i][0];
+                        }
+                    }
+
+                    let shortPackage = '';
+
+                    for (let i = 0; i < packageCnt; i++)
+                        shortPackage += packages[i] + '.';
+
+                    const className = packages[packageCnt];
+
+                    const classLen = className.length;
+
+                    let minLen = Math.min(classLen, 3);
+
+                    totalWidth = measureText(shortPackage + className);
+
+                    // Compact class name if shorten package path is very long.
+                    if (totalWidth > widthByName[nameIx]) {
+                        let maxLen = classLen;
+                        let middleLen = (minLen + (maxLen - minLen) / 2 ) | 0;
+
+                        while (middleLen !== minLen && middleLen !== maxLen) {
+                            const middleLenPx = measureText(shortPackage + className.substr(0, middleLen) + '...');
+
+                            if (middleLenPx > widthByName[nameIx])
+                                maxLen = middleLen;
+                            else
+                                minLen = middleLen;
+
+                            middleLen = (minLen + (maxLen - minLen) / 2 ) | 0;
+                        }
+
+                        names[nameIx] = shortPackage + className.substring(0, middleLen) + '...';
+                    }
+                    else
+                        names[nameIx] = shortPackage + className;
+                }
+            }
+        }
+
+        return names;
+    }
+
+    /**
+     * Compact any string by max number of pixels.
+     *
+     * @param label String to compact.
+     * @param nameWidth Maximum available width in pixels for simple name.
+     * @returns {*} Compacted string.
+     */
+    function compactLabelByPixels(label, nameWidth) {
+        if (nameWidth <= 0)
+            return label;
+
+        const totalWidth = measureText(label);
+
+        if (totalWidth > nameWidth) {
+            let maxLen = label.length;
+            let minLen = Math.min(maxLen, 3);
+            let middleLen = (minLen + (maxLen - minLen) / 2 ) | 0;
+
+            while (middleLen !== minLen && middleLen !== maxLen) {
+                const middleLenPx = measureText(label.substr(0, middleLen) + '...');
+
+                if (middleLenPx > nameWidth)
+                    maxLen = middleLen;
+                else
+                    minLen = middleLen;
+
+                middleLen = (minLen + (maxLen - minLen) / 2 ) | 0;
+            }
+
+            return label.substring(0, middleLen) + '...';
+        }
+
+        return label;
+    }
+
+    /**
+     * Calculate available width for text in link to edit element.
+     *
+     * @param index Showed index of element for calculation of maximum width in pixels.
+     * @param id Id of contains link table.
+     * @returns {*[]} First element is length of class for single value, second element is length for pair vlaue.
+     */
+    function availableWidth(index, id) {
+        const idElem = $('#' + id);
+
+        let width = 0;
+
+        switch (idElem.prop('tagName')) {
+            // Detection of available width in presentation table row.
+            case 'TABLE':
+                const cont = $(idElem.find('tr')[index - 1]).find('td')[0];
+
+                width = cont.clientWidth;
+
+                if (width > 0) {
+                    const children = $(cont).children(':not("a")');
+
+                    _.forEach(children, function(child) {
+                        if ('offsetWidth' in child)
+                            width -= $(child).outerWidth(true);
+                    });
+                }
+
+                break;
+
+            // Detection of available width in dropdown row.
+            case 'A':
+                width = idElem.width();
+
+                $(idElem).children(':not("span")').each(function(ix, child) {
+                    if ('offsetWidth' in child)
+                        width -= child.offsetWidth;
+                });
+
+                break;
+
+            default:
+        }
+
+        return width | 0;
+    }
+
+    // TODO: move somewhere else
+    function triggerValidation(form) {
+        const fe = (m) => Object.keys(m.$error)[0];
+        const em = (e) => (m) => {
+            if (!e)
+                return;
+
+            const walk = (m) => {
+                if (!m || !m.$error[e])
+                    return;
+
+                if (m.$error[e] === true)
+                    return m;
+
+                return walk(m.$error[e][0]);
+            };
+
+            return walk(m);
+        };
+
+        $rootScope.$broadcast('$showValidationError', em(fe(form))(form));
+    }
+
+    return {
+        /**
+         * Cut class name by width in pixel or width in symbol count.
+         *
+         * @param id Id of parent table.
+         * @param index Row number in table.
+         * @param maxLength Maximum length in symbols for all names.
+         * @param names Array of class names to compact.
+         * @param divider String to visualy divide items.
+         * @returns {*} Array of compacted class names.
+         */
+        compactJavaName(id, index, maxLength, names, divider) {
+            divider = ' ' + divider + ' ';
+
+            const prefix = index + ') ';
+
+            const nameCnt = names.length;
+
+            const nameLength = ((maxLength - 3 * (nameCnt - 1)) / nameCnt) | 0;
+
+            try {
+                const nameWidth = (availableWidth(index, id) - measureText(prefix) - (nameCnt - 1) * measureText(divider)) /
+                    nameCnt | 0;
+
+                // HTML5 calculation of showed message width.
+                names = compactByMaxPixels(names, nameLength, nameWidth);
+            }
+            catch (err) {
+                names = compactByMaxCharts(names, nameLength);
+            }
+
+            let result = prefix + names[0];
+
+            for (let nameIx = 1; nameIx < names.length; nameIx++)
+                result += divider + names[nameIx];
+
+            return result;
+        },
+        /**
+         * Compact text by width in pixels or symbols count.
+         *
+         * @param id Id of parent table.
+         * @param index Row number in table.
+         * @param maxLength Maximum length in symbols for all names.
+         * @param label Text to compact.
+         * @returns Compacted label text.
+         */
+        compactTableLabel(id, index, maxLength, label) {
+            label = index + ') ' + label;
+
+            try {
+                const nameWidth = availableWidth(index, id) | 0;
+
+                // HTML5 calculation of showed message width.
+                label = compactLabelByPixels(label, nameWidth);
+            }
+            catch (err) {
+                const nameLength = maxLength - 3 | 0;
+
+                label = label.length > maxLength ? label.substr(0, nameLength) + '...' : label;
+            }
+
+            return label;
+        },
+        widthIsSufficient(id, index, text) {
+            try {
+                const available = availableWidth(index, id);
+
+                const required = measureText(text);
+
+                return !available || available >= Math.floor(required);
+            }
+            catch (err) {
+                return true;
+            }
+        },
+        ensureActivePanel(panels, id, focusId) {
+            ensureActivePanel(panels, id, focusId);
+        },
+        saveBtnTipText(dirty, objectName) {
+            if (dirty)
+                return 'Save ' + objectName;
+
+            return 'Nothing to save';
+        },
+        formUI() {
+            return {
+                ready: false,
+                expanded: false,
+                loadedPanels: [],
+                loadPanel(pnl) {
+                    if (!_.includes(this.loadedPanels, pnl))
+                        this.loadedPanels.push(pnl);
+                },
+                isPanelLoaded(pnl) {
+                    return _.includes(this.loadedPanels, pnl);
+                }
+            };
+        },
+        markPristineInvalidAsDirty(ngModelCtrl) {
+            if (ngModelCtrl && ngModelCtrl.$invalid && ngModelCtrl.$pristine)
+                ngModelCtrl.$setDirty();
+        },
+        triggerValidation
+    };
+}
+
+service.$inject = ['$window', 'IgniteFocus', '$rootScope'];
diff --git a/modules/frontend/app/services/InetAddress.service.js b/modules/frontend/app/services/InetAddress.service.js
new file mode 100644
index 0000000..1f50bdf
--- /dev/null
+++ b/modules/frontend/app/services/InetAddress.service.js
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+export default function() {
+    return {
+        /**
+         * @param {String} ip IP address to check.
+         * @returns {boolean} 'true' if given ip address is valid.
+         */
+        validIp(ip) {
+            const regexp = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/;
+
+            return regexp.test(ip);
+        },
+        /**
+         * @param {String} hostNameOrIp host name or ip address to check.
+         * @returns {boolean} 'true' if given is host name or ip.
+         */
+        validHost(hostNameOrIp) {
+            const regexp = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
+
+            return regexp.test(hostNameOrIp) || this.validIp(hostNameOrIp);
+        },
+        /**
+         * @param {number} port Port value to check.
+         * @returns boolean 'true' if given port is valid tcp/udp port range.
+         */
+        validPort(port) {
+            return _.isInteger(port) && port > 0 && port <= 65535;
+        },
+        /**
+         * @param {number} port Port value to check.
+         * @returns {boolean} 'true' if given port in non system port range(user+dynamic).
+         */
+        validNonSystemPort(port) {
+            return _.isInteger(port) && port >= 1024 && port <= 65535;
+        }
+    };
+}
diff --git a/modules/frontend/app/services/JavaTypes.service.js b/modules/frontend/app/services/JavaTypes.service.js
new file mode 100644
index 0000000..884e8ca
--- /dev/null
+++ b/modules/frontend/app/services/JavaTypes.service.js
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+import includes from 'lodash/includes';
+import isNil from 'lodash/isNil';
+import find from 'lodash/find';
+// Java built-in class names.
+import JAVA_CLASSES from '../data/java-classes.json';
+// Java build-in primitives.
+import JAVA_PRIMITIVES from '../data/java-primitives.json';
+// Java keywords.
+import JAVA_KEYWORDS from '../data/java-keywords.json';
+
+// Regular expression to check Java identifier.
+const VALID_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/im;
+
+// Regular expression to check Java class name.
+const VALID_CLASS_NAME = /^(([a-zA-Z_$][a-zA-Z0-9_$]*)\.)*([a-zA-Z_$][a-zA-Z0-9_$]*)$/im;
+
+// Regular expression to check Java package.
+const VALID_PACKAGE = /^(([a-zA-Z_$][a-zA-Z0-9_$]*)\.)*([a-zA-Z_$][a-zA-Z0-9_$]*(\.?\*)?)$/im;
+
+// Regular expression to check UUID string representation.
+const VALID_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/im;
+
+// Extended list of Java built-in class names.
+const JAVA_CLASS_STRINGS = JAVA_CLASSES.slice();
+
+/**
+ * Utility service for various check on java types.
+ */
+export default class JavaTypes {
+    constructor() {
+        JAVA_CLASS_STRINGS.push({short: 'byte[]', full: 'byte[]', stringValue: '[B'});
+    }
+
+    /**
+     * @param clsName {String} Class name to check.
+     * @returns {boolean} 'true' if provided class name is a not Java built in class.
+     */
+    nonBuiltInClass(clsName) {
+        return isNil(find(JAVA_CLASSES, (clazz) => clsName === clazz.short || clsName === clazz.full));
+    }
+
+    /**
+     * @param clsName Class name to check.
+     * @returns {String} Full class name for java build-in types or source class otherwise.
+     */
+    fullClassName(clsName) {
+        const type = find(JAVA_CLASSES, (clazz) => clsName === clazz.short);
+
+        return type ? type.full : clsName;
+    }
+
+    /**
+     * @param clsName Class name to check.
+     * @returns {String} Full class name string presentation for java build-in types or source class otherwise.
+     */
+    stringClassName(clsName) {
+        const type = _.find(JAVA_CLASS_STRINGS, (clazz) => clsName === clazz.short);
+
+        return type ? type.stringValue || type.full : clsName;
+    }
+
+    /**
+     * Extract class name from full class name.
+     *
+     * @param clsName full class name.
+     * @return {String} Class name.
+     */
+    shortClassName(clsName) {
+        const dotIdx = clsName.lastIndexOf('.');
+
+        return dotIdx > 0 ? clsName.substr(dotIdx + 1) : clsName;
+    }
+
+    /**
+     * @param value {String} Value text to check.
+     * @returns {boolean} 'true' if given text is valid Java class name.
+     */
+    validIdentifier(value) {
+        return !!(value && VALID_IDENTIFIER.test(value));
+    }
+
+    /**
+     * @param value {String} Value text to check.
+     * @returns {boolean} 'true' if given text is valid Java class name.
+     */
+    validClassName(value) {
+        return !!(value && VALID_CLASS_NAME.test(value));
+    }
+
+    /**
+     * @param value {String} Value text to check.
+     * @returns {boolean} 'true' if given text is valid Java package.
+     */
+    validPackage(value) {
+        return !!(value && VALID_PACKAGE.test(value));
+    }
+
+    /**
+     * @param value {String} Value text to check.
+     * @returns {boolean} 'true' if given text is valid Java UUID value.
+     */
+    validUUID(value) {
+        return !!(value && VALID_UUID.test(value));
+    }
+
+    /**
+     * @param value {String} Value text to check.
+     * @returns {boolean} 'true' if given text is a Java type with package.
+     */
+    packageSpecified(value) {
+        return value.split('.').length >= 2;
+    }
+
+    /**
+     * @param value {String} Value text to check.
+     * @returns {boolean} 'true' if given value is one of Java reserved keywords.
+     */
+    isKeyword(value) {
+        return !!(value && includes(JAVA_KEYWORDS, value.toLowerCase()));
+    }
+
+    /**
+     * @param {String} clsName Class name to check.
+     * @returns {boolean} 'true' if given class name is java primitive.
+     */
+    isPrimitive(clsName) {
+        return includes(JAVA_PRIMITIVES, clsName);
+    }
+
+    /**
+     * Convert some name to valid java name.
+     *
+     * @param prefix To append to java name.
+     * @param name to convert.
+     * @returns {string} Valid java name.
+     */
+    toJavaName(prefix, name) {
+        const javaName = name ? this.shortClassName(name).replace(/[^A-Za-z_0-9]+/g, '_') : 'dflt';
+
+        return prefix + javaName.charAt(0).toLocaleUpperCase() + javaName.slice(1);
+    }
+}
diff --git a/modules/frontend/app/services/JavaTypes.spec.js b/modules/frontend/app/services/JavaTypes.spec.js
new file mode 100644
index 0000000..122df17
--- /dev/null
+++ b/modules/frontend/app/services/JavaTypes.spec.js
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import JavaTypes from './JavaTypes.service';
+import {assert} from 'chai';
+
+const instance = new JavaTypes();
+
+suite('JavaTypesTestsSuite', () => {
+    test('nonBuiltInClass', () => {
+        assert.equal(instance.nonBuiltInClass('BigDecimal'), false);
+        assert.equal(instance.nonBuiltInClass('java.math.BigDecimal'), false);
+
+        assert.equal(instance.nonBuiltInClass('String'), false);
+        assert.equal(instance.nonBuiltInClass('java.lang.String'), false);
+
+        assert.equal(instance.nonBuiltInClass('Timestamp'), false);
+        assert.equal(instance.nonBuiltInClass('java.sql.Timestamp'), false);
+
+        assert.equal(instance.nonBuiltInClass('Date'), false);
+        assert.equal(instance.nonBuiltInClass('java.sql.Date'), false);
+
+        assert.equal(instance.nonBuiltInClass('Date'), false);
+        assert.equal(instance.nonBuiltInClass('java.util.Date'), false);
+
+        assert.equal(instance.nonBuiltInClass('CustomClass'), true);
+        assert.equal(instance.nonBuiltInClass('java.util.CustomClass'), true);
+        assert.equal(instance.nonBuiltInClass('my.package.CustomClass'), true);
+    });
+
+    test('shortClassName', () => {
+        assert.equal(instance.shortClassName('java.math.BigDecimal'), 'BigDecimal');
+        assert.equal(instance.shortClassName('BigDecimal'), 'BigDecimal');
+        assert.equal(instance.shortClassName('int'), 'int');
+        assert.equal(instance.shortClassName('java.lang.Integer'), 'Integer');
+        assert.equal(instance.shortClassName('Integer'), 'Integer');
+        assert.equal(instance.shortClassName('java.util.UUID'), 'UUID');
+        assert.equal(instance.shortClassName('java.sql.Date'), 'Date');
+        assert.equal(instance.shortClassName('Date'), 'Date');
+        assert.equal(instance.shortClassName('com.my.Abstract'), 'Abstract');
+        assert.equal(instance.shortClassName('Abstract'), 'Abstract');
+    });
+
+    test('fullClassName', () => {
+        assert.equal(instance.fullClassName('BigDecimal'), 'java.math.BigDecimal');
+    });
+
+    test('validIdentifier', () => {
+        assert.equal(instance.validIdentifier('myIdent'), true);
+        assert.equal(instance.validIdentifier('java.math.BigDecimal'), false);
+        assert.equal(instance.validIdentifier('2Demo'), false);
+        assert.equal(instance.validIdentifier('abra kadabra'), false);
+        assert.equal(instance.validIdentifier(), false);
+        assert.equal(instance.validIdentifier(null), false);
+        assert.equal(instance.validIdentifier(''), false);
+        assert.equal(instance.validIdentifier(' '), false);
+    });
+
+    test('validClassName', () => {
+        assert.equal(instance.validClassName('java.math.BigDecimal'), true);
+        assert.equal(instance.validClassName('2Demo'), false);
+        assert.equal(instance.validClassName('abra kadabra'), false);
+        assert.equal(instance.validClassName(), false);
+        assert.equal(instance.validClassName(null), false);
+        assert.equal(instance.validClassName(''), false);
+        assert.equal(instance.validClassName(' '), false);
+    });
+
+    test('validPackage', () => {
+        assert.equal(instance.validPackage('java.math.BigDecimal'), true);
+        assert.equal(instance.validPackage('my.org.SomeClass'), true);
+        assert.equal(instance.validPackage('25'), false);
+        assert.equal(instance.validPackage('abra kadabra'), false);
+        assert.equal(instance.validPackage(''), false);
+        assert.equal(instance.validPackage(' '), false);
+    });
+
+    test('packageSpecified', () => {
+        assert.equal(instance.packageSpecified('java.math.BigDecimal'), true);
+        assert.equal(instance.packageSpecified('BigDecimal'), false);
+    });
+
+    test('isKeyword', () => {
+        assert.equal(instance.isKeyword('abstract'), true);
+        assert.equal(instance.isKeyword('Abstract'), true);
+        assert.equal(instance.isKeyword('abra kadabra'), false);
+        assert.equal(instance.isKeyword(), false);
+        assert.equal(instance.isKeyword(null), false);
+        assert.equal(instance.isKeyword(''), false);
+        assert.equal(instance.isKeyword(' '), false);
+    });
+
+    test('isPrimitive', () => {
+        assert.equal(instance.isPrimitive('boolean'), true);
+    });
+
+    test('validUUID', () => {
+        assert.equal(instance.validUUID('123e4567-e89b-12d3-a456-426655440000'), true);
+        assert.equal(instance.validUUID('12345'), false);
+        assert.equal(instance.validUUID(), false);
+        assert.equal(instance.validUUID(null), false);
+        assert.equal(instance.validUUID(''), false);
+        assert.equal(instance.validUUID(' '), false);
+    });
+});
diff --git a/modules/frontend/app/services/LegacyTable.service.js b/modules/frontend/app/services/LegacyTable.service.js
new file mode 100644
index 0000000..3086f4f
--- /dev/null
+++ b/modules/frontend/app/services/LegacyTable.service.js
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// TODO: Refactor this service for legacy tables with more than one input field.
+/**
+ * @param {ReturnType<typeof import('./LegacyUtils.service').default>} LegacyUtils
+ * @param {ReturnType<typeof import('./Focus.service').default>} Focus
+ * @param {import('./ErrorPopover.service').default} ErrorPopover
+ */
+export default function service(LegacyUtils, Focus, ErrorPopover) {
+    function _model(item, field) {
+        let path = field.path;
+
+        if (_.isNil(path) || _.isNil(item))
+            return item;
+
+        path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
+        path = path.replace(/^\./, ''); // strip a leading dot
+
+        const segs = path.split('.');
+        let root = item;
+
+        while (segs.length > 0) {
+            const pathStep = segs.shift();
+
+            if (typeof root[pathStep] === 'undefined')
+                root[pathStep] = {};
+
+            root = root[pathStep];
+        }
+
+        return root;
+    }
+
+    const table = {name: 'none', editIndex: -1};
+
+    function _tableReset() {
+        delete table.field;
+        table.name = 'none';
+        table.editIndex = -1;
+
+        ErrorPopover.hide();
+    }
+
+    function _tableSaveAndReset() {
+        const field = table.field;
+
+        const save = LegacyUtils.isDefined(field) && LegacyUtils.isDefined(field.save);
+
+        if (!save || !LegacyUtils.isDefined(field) || field.save(field, table.editIndex, true)) {
+            _tableReset();
+
+            return true;
+        }
+
+        return false;
+    }
+
+    function _tableState(field, editIndex, specName) {
+        table.field = field;
+        table.name = specName || field.model;
+        table.editIndex = editIndex;
+    }
+
+    function _tableUI(tbl) {
+        const ui = tbl.ui;
+
+        return ui ? ui : tbl.type;
+    }
+
+    function _tableFocus(focusId, index) {
+        Focus.move((index < 0 ? 'new' : 'cur') + focusId + (index >= 0 ? index : ''));
+    }
+
+    function _tablePairValue(filed, index) {
+        return index < 0 ? {key: filed.newKey, value: filed.newValue} : {
+            key: filed.curKey,
+            value: filed.curValue
+        };
+    }
+
+    function _tableStartEdit(item, tbl, index, save) {
+        _tableState(tbl, index);
+
+        const val = _.get(_model(item, tbl), tbl.model)[index];
+
+        const ui = _tableUI(tbl);
+
+        tbl.save = save;
+
+        if (ui === 'table-pair') {
+            tbl.curKey = val[tbl.keyName];
+            tbl.curValue = val[tbl.valueName];
+
+            _tableFocus('Key' + tbl.focusId, index);
+        }
+        else if (ui === 'table-db-fields') {
+            tbl.curDatabaseFieldName = val.databaseFieldName;
+            tbl.curDatabaseFieldType = val.databaseFieldType;
+            tbl.curJavaFieldName = val.javaFieldName;
+            tbl.curJavaFieldType = val.javaFieldType;
+
+            _tableFocus('DatabaseFieldName' + tbl.focusId, index);
+        }
+        else if (ui === 'table-indexes') {
+            tbl.curIndexName = val.name;
+            tbl.curIndexType = val.indexType;
+            tbl.curIndexFields = val.fields;
+
+            _tableFocus(tbl.focusId, index);
+        }
+    }
+
+    function _tableNewItem(tbl) {
+        _tableState(tbl, -1);
+
+        const ui = _tableUI(tbl);
+
+        if (ui === 'table-pair') {
+            tbl.newKey = null;
+            tbl.newValue = null;
+
+            _tableFocus('Key' + tbl.focusId, -1);
+        }
+        else if (ui === 'table-db-fields') {
+            tbl.newDatabaseFieldName = null;
+            tbl.newDatabaseFieldType = null;
+            tbl.newJavaFieldName = null;
+            tbl.newJavaFieldType = null;
+
+            _tableFocus('DatabaseFieldName' + tbl.focusId, -1);
+        }
+        else if (ui === 'table-indexes') {
+            tbl.newIndexName = null;
+            tbl.newIndexType = 'SORTED';
+            tbl.newIndexFields = null;
+
+            _tableFocus(tbl.focusId, -1);
+        }
+    }
+
+    return {
+        tableState: _tableState,
+        tableReset: _tableReset,
+        tableSaveAndReset: _tableSaveAndReset,
+        tableNewItem: _tableNewItem,
+        tableNewItemActive(tbl) {
+            return table.name === tbl.model && table.editIndex < 0;
+        },
+        tableEditing(tbl, index) {
+            return table.name === tbl.model && table.editIndex === index;
+        },
+        tableEditedRowIndex() {
+            return table.editIndex;
+        },
+        tableField() {
+            return table.field;
+        },
+        tableStartEdit: _tableStartEdit,
+        tableRemove(item, field, index) {
+            _tableReset();
+
+            _.get(_model(item, field), field.model).splice(index, 1);
+        },
+        tablePairValue: _tablePairValue,
+        tablePairSave(pairValid, item, field, index, stopEdit) {
+            const valid = pairValid(item, field, index, stopEdit);
+
+            if (valid) {
+                const pairValue = _tablePairValue(field, index);
+
+                let pairModel = {};
+
+                const container = _.get(item, field.model);
+
+                if (index < 0) {
+                    pairModel[field.keyName] = pairValue.key;
+                    pairModel[field.valueName] = pairValue.value;
+
+                    if (container)
+                        container.push(pairModel);
+                    else
+                        _.set(item, field.model, [pairModel]);
+
+                    if (!stopEdit)
+                        _tableNewItem(field);
+                }
+                else {
+                    pairModel = container[index];
+
+                    pairModel[field.keyName] = pairValue.key;
+                    pairModel[field.valueName] = pairValue.value;
+
+                    if (!stopEdit) {
+                        if (index < container.length - 1)
+                            _tableStartEdit(item, field, index + 1);
+                        else
+                            _tableNewItem(field);
+                    }
+                }
+            }
+
+            return valid || stopEdit;
+        },
+        tablePairSaveVisible(field, index) {
+            const pairValue = _tablePairValue(field, index);
+
+            return !LegacyUtils.isEmptyString(pairValue.key) && !LegacyUtils.isEmptyString(pairValue.value);
+        },
+        tableFocusInvalidField(index, id) {
+            _tableFocus(id, index);
+
+            return false;
+        },
+        tableFieldId(index, id) {
+            return (index < 0 ? 'new' : 'cur') + id + (index >= 0 ? index : '');
+        }
+    };
+}
+
+service.$inject = ['IgniteLegacyUtils', 'IgniteFocus', 'IgniteErrorPopover'];
diff --git a/modules/frontend/app/services/LegacyUtils.service.js b/modules/frontend/app/services/LegacyUtils.service.js
new file mode 100644
index 0000000..8362669
--- /dev/null
+++ b/modules/frontend/app/services/LegacyUtils.service.js
@@ -0,0 +1,556 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import saver from 'file-saver';
+import _ from 'lodash';
+
+// TODO: Refactor this service for legacy tables with more than one input field.
+/**
+ * @param {import('./ErrorPopover.service').default} ErrorPopover
+ */
+export default function service(ErrorPopover) {
+    function isDefined(v) {
+        return !_.isNil(v);
+    }
+
+    function isEmptyString(s) {
+        if (isDefined(s))
+            return s.trim().length === 0;
+
+        return true;
+    }
+
+    const javaBuiltInClasses = [
+        'BigDecimal',
+        'Boolean',
+        'Byte',
+        'Date',
+        'Double',
+        'Float',
+        'Integer',
+        'Long',
+        'Object',
+        'Short',
+        'String',
+        'Time',
+        'Timestamp',
+        'UUID'
+    ];
+
+    const javaBuiltInTypes = [
+        'BigDecimal',
+        'boolean',
+        'Boolean',
+        'byte',
+        'byte[]',
+        'Byte',
+        'Date',
+        'double',
+        'Double',
+        'float',
+        'Float',
+        'int',
+        'Integer',
+        'long',
+        'Long',
+        'Object',
+        'short',
+        'Short',
+        'String',
+        'Time',
+        'Timestamp',
+        'UUID'
+    ];
+
+    const javaBuiltInFullNameClasses = [
+        'java.math.BigDecimal',
+        'java.lang.Boolean',
+        'java.lang.Byte',
+        'java.sql.Date',
+        'java.lang.Double',
+        'java.lang.Float',
+        'java.lang.Integer',
+        'java.lang.Long',
+        'java.lang.Object',
+        'java.lang.Short',
+        'java.lang.String',
+        'java.sql.Time',
+        'java.sql.Timestamp',
+        'java.util.UUID'
+    ];
+
+    /**
+     * @param clsName Class name to check.
+     * @param additionalClasses List of classes to check as builtin.
+     * @returns {Boolean} 'true' if given class name is a java build-in type.
+     */
+    function isJavaBuiltInClass(clsName, additionalClasses) {
+        if (isEmptyString(clsName))
+            return false;
+
+        return _.includes(javaBuiltInClasses, clsName) || _.includes(javaBuiltInFullNameClasses, clsName)
+            || (_.isArray(additionalClasses) && _.includes(additionalClasses, clsName));
+    }
+
+    const SUPPORTED_JDBC_TYPES = [
+        'BIGINT',
+        'BIT',
+        'BOOLEAN',
+        'BLOB',
+        'CHAR',
+        'CLOB',
+        'DATE',
+        'DECIMAL',
+        'DOUBLE',
+        'FLOAT',
+        'INTEGER',
+        'LONGNVARCHAR',
+        'LONGVARCHAR',
+        'NCHAR',
+        'NUMERIC',
+        'NVARCHAR',
+        'REAL',
+        'SMALLINT',
+        'TIME',
+        'TIMESTAMP',
+        'TINYINT',
+        'VARCHAR'
+    ];
+
+    /*eslint-disable */
+    const JAVA_KEYWORDS = [
+        'abstract',
+        'assert',
+        'boolean',
+        'break',
+        'byte',
+        'case',
+        'catch',
+        'char',
+        'class',
+        'const',
+        'continue',
+        'default',
+        'do',
+        'double',
+        'else',
+        'enum',
+        'extends',
+        'false',
+        'final',
+        'finally',
+        'float',
+        'for',
+        'goto',
+        'if',
+        'implements',
+        'import',
+        'instanceof',
+        'int',
+        'interface',
+        'long',
+        'native',
+        'new',
+        'null',
+        'package',
+        'private',
+        'protected',
+        'public',
+        'return',
+        'short',
+        'static',
+        'strictfp',
+        'super',
+        'switch',
+        'synchronized',
+        'this',
+        'throw',
+        'throws',
+        'transient',
+        'true',
+        'try',
+        'void',
+        'volatile',
+        'while'
+    ];
+    /* eslint-enable */
+
+    const VALID_JAVA_IDENTIFIER = new RegExp('^[a-zA-Z_$][a-zA-Z\\d_$]*$');
+
+    function isValidJavaIdentifier(msg, ident, elemId, panels, panelId, stopEdit) {
+        if (isEmptyString(ident))
+            return !stopEdit && ErrorPopover.show(elemId, msg + ' is invalid!', panels, panelId);
+
+        if (_.includes(JAVA_KEYWORDS, ident))
+            return !stopEdit && ErrorPopover.show(elemId, msg + ' could not contains reserved java keyword: "' + ident + '"!', panels, panelId);
+
+        if (!VALID_JAVA_IDENTIFIER.test(ident))
+            return !stopEdit && ErrorPopover.show(elemId, msg + ' contains invalid identifier: "' + ident + '"!', panels, panelId);
+
+        return true;
+    }
+
+    /**
+     * Extract datasource from cache or cluster.
+     *
+     * @param object Cache or cluster to extract datasource.
+     * @returns {*} Datasource object or null if not set.
+     */
+    function extractDataSource(object) {
+        let datasource = null;
+
+        // Extract from cluster object
+        if (_.get(object, 'discovery.kind') === 'Jdbc') {
+            datasource = object.discovery.Jdbc;
+
+            if (datasource.dataSourceBean && datasource.dialect)
+                return datasource;
+        } // Extract from JDBC checkpoint configuration.
+        else if (_.get(object, 'kind') === 'JDBC') {
+            datasource = object.JDBC;
+
+            if (datasource.dataSourceBean && datasource.dialect)
+                return datasource;
+        } // Extract from cache object
+        else if (_.get(object, 'cacheStoreFactory.kind')) {
+            datasource = object.cacheStoreFactory[object.cacheStoreFactory.kind];
+
+            if (datasource.dialect || (datasource.connectVia === 'DataSource'))
+                return datasource;
+        }
+
+        return null;
+    }
+
+    const cacheStoreJdbcDialects = [
+        {value: 'Generic', label: 'Generic JDBC'},
+        {value: 'Oracle', label: 'Oracle'},
+        {value: 'DB2', label: 'IBM DB2'},
+        {value: 'SQLServer', label: 'Microsoft SQL Server'},
+        {value: 'MySQL', label: 'MySQL'},
+        {value: 'PostgreSQL', label: 'PostgreSQL'},
+        {value: 'H2', label: 'H2 database'}
+    ];
+
+    function domainForStoreConfigured(domain) {
+        const isEmpty = !isDefined(domain) || (isEmptyString(domain.databaseSchema) &&
+            isEmptyString(domain.databaseTable) &&
+            _.isEmpty(domain.keyFields) &&
+            _.isEmpty(domain.valueFields));
+
+        return !isEmpty;
+    }
+
+    const DS_CHECK_SUCCESS = {checked: true};
+
+    /**
+     * Compare datasources of caches or clusters.
+     *
+     * @param firstObj First cache or cluster.
+     * @param firstType Type of first object to compare.
+     * @param secondObj Second cache or cluster.
+     * @param secondType Type of first object to compare.
+     * @param index Index of invalid object when check is failed.
+     * @returns {*} Check result object.
+     */
+    function compareDataSources(firstObj, firstType, secondObj, secondType, index) {
+        const firstDs = extractDataSource(firstObj);
+        const secondDs = extractDataSource(secondObj);
+
+        if (firstDs && secondDs) {
+            const firstDB = firstDs.dialect;
+            const secondDB = secondDs.dialect;
+
+            if (firstDs.dataSourceBean === secondDs.dataSourceBean && firstDB !== secondDB)
+                return {checked: false, firstObj, firstDs, firstType, secondObj, secondDs, secondType, index};
+        }
+
+        return DS_CHECK_SUCCESS;
+    }
+
+    function compareSQLSchemaNames(firstCache, secondCache) {
+        const firstName = firstCache.sqlSchema;
+        const secondName = secondCache.sqlSchema;
+
+        if (firstName && secondName && (firstName === secondName))
+            return {checked: false, firstCache, secondCache};
+
+        return DS_CHECK_SUCCESS;
+    }
+
+    function toJavaName(prefix, name) {
+        const javaName = name ? name.replace(/[^A-Za-z_0-9]+/g, '_') : 'dflt';
+
+        return prefix + javaName.charAt(0).toLocaleUpperCase() + javaName.slice(1);
+    }
+
+    return {
+        VALID_JAVA_IDENTIFIER,
+        JAVA_KEYWORDS,
+        mkOptions(options) {
+            return _.map(options, (option) => {
+                return {value: option, label: isDefined(option) ? option : 'Not set'};
+            });
+        },
+        isDefined,
+        hasProperty(obj, props) {
+            for (const propName in props) {
+                if (props.hasOwnProperty(propName)) {
+                    if (obj[propName])
+                        return true;
+                }
+            }
+
+            return false;
+        },
+        isEmptyString,
+        SUPPORTED_JDBC_TYPES,
+        javaBuiltInClasses,
+        javaBuiltInTypes,
+        isJavaBuiltInClass,
+        isValidJavaIdentifier,
+        isValidJavaClass(msg, ident, allowBuiltInClass, elemId, packageOnly, panels, panelId, stopEdit = false) {
+            if (isEmptyString(ident))
+                return !stopEdit && ErrorPopover.show(elemId, msg + ' could not be empty!', panels, panelId);
+
+            const parts = ident.split('.');
+
+            const len = parts.length;
+
+            if (!allowBuiltInClass && isJavaBuiltInClass(ident))
+                return !stopEdit && ErrorPopover.show(elemId, msg + ' should not be the Java build-in class!', panels, panelId);
+
+            if (len < 2) {
+                if (isJavaBuiltInClass(ident, allowBuiltInClass))
+                    return true;
+
+                if (!packageOnly)
+                    return !stopEdit && ErrorPopover.show(elemId, msg + ' does not have package specified!', panels, panelId);
+            }
+
+            for (let i = 0; i < parts.length; i++) {
+                const part = parts[i];
+
+                if (!isValidJavaIdentifier(msg, part, elemId, panels, panelId, stopEdit))
+                    return false;
+            }
+
+            return true;
+        },
+        domainForQueryConfigured(domain) {
+            const isEmpty = !isDefined(domain) || (_.isEmpty(domain.fields) &&
+                _.isEmpty(domain.aliases) &&
+                _.isEmpty(domain.indexes));
+
+            return !isEmpty;
+        },
+        domainForStoreConfigured,
+        download(type = 'application/octet-stream', name = 'file.txt', data = '') {
+            const file = new Blob([data], { type: `${type};charset=utf-8`});
+
+            saver.saveAs(file, name, false);
+        },
+        getQueryVariable(name) {
+            const attrs = window.location.search.substring(1).split('&');
+            const attr = _.find(attrs, (a) => a === name || (a.indexOf('=') >= 0 && a.substr(0, a.indexOf('=')) === name));
+
+            if (!isDefined(attr))
+                return null;
+
+            if (attr === name)
+                return true;
+
+            return attr.substr(attr.indexOf('=') + 1);
+        },
+        cacheStoreJdbcDialects,
+        cacheStoreJdbcDialectsLabel(dialect) {
+            const found = _.find(cacheStoreJdbcDialects, (dialectVal) => dialectVal.value === dialect);
+
+            return found ? found.label : null;
+        },
+        checkDataSources(cluster, caches, checkCacheExt) {
+            let res = DS_CHECK_SUCCESS;
+
+            _.find(caches, (curCache, curIx) => {
+                // Check datasources of cluster JDBC ip finder and cache store factory datasource.
+                res = compareDataSources(curCache, 'cache', cluster, 'cluster');
+
+                if (!res.checked)
+                    return true;
+
+                _.find(cluster.checkpointSpi, (spi, spiIx) => {
+                    res = compareDataSources(curCache, 'cache', spi, 'checkpoint', spiIx);
+
+                    return !res.checked;
+                });
+
+                if (!res.checked)
+                    return true;
+
+                // Check datasource of current saved cache and datasource of other cache in cluster.
+                if (isDefined(checkCacheExt)) {
+                    if (checkCacheExt._id !== curCache._id) {
+                        res = compareDataSources(checkCacheExt, 'cache', curCache, 'cache');
+
+                        return !res.checked;
+                    }
+
+                    return false;
+                }
+
+                // Check datasources of specified list of caches.
+                return _.find(caches, (checkCache, checkIx) => {
+                    if (checkIx < curIx) {
+                        res = compareDataSources(checkCache, 'cache', curCache, 'cache');
+
+                        return !res.checked;
+                    }
+
+                    return false;
+                });
+            });
+
+            if (res.checked) {
+                _.find(cluster.checkpointSpi, (curSpi, curIx) => {
+                    // Check datasources of cluster JDBC ip finder and cache store factory datasource.
+                    res = compareDataSources(cluster, 'cluster', curSpi, 'checkpoint', curIx);
+
+                    if (!res.checked)
+                        return true;
+
+                    _.find(cluster.checkpointSpi, (spi, spiIx) => {
+                        if (spiIx < curIx) {
+                            res = compareDataSources(curSpi, 'checkpoint', spi, 'checkpoint', curIx);
+
+                            return !res.checked;
+                        }
+
+                        return false;
+                    });
+                });
+            }
+
+            return res;
+        },
+        checkCacheSQLSchemas(caches, checkCacheExt) {
+            let res = DS_CHECK_SUCCESS;
+
+            _.find(caches, (curCache, curIx) => {
+                if (isDefined(checkCacheExt)) {
+                    if (checkCacheExt._id !== curCache._id) {
+                        res = compareSQLSchemaNames(checkCacheExt, curCache);
+
+                        return !res.checked;
+                    }
+
+                    return false;
+                }
+
+                return _.find(caches, (checkCache, checkIx) => {
+                    if (checkIx < curIx) {
+                        res = compareSQLSchemaNames(checkCache, curCache);
+
+                        return !res.checked;
+                    }
+
+                    return false;
+                });
+            });
+
+            return res;
+        },
+        autoCacheStoreConfiguration(cache, domains) {
+            const cacheStoreFactory = isDefined(cache.cacheStoreFactory) &&
+                isDefined(cache.cacheStoreFactory.kind);
+
+            if (!cacheStoreFactory && _.findIndex(domains, domainForStoreConfigured) >= 0) {
+                const dflt = !cache.readThrough && !cache.writeThrough;
+
+                return {
+                    cacheStoreFactory: {
+                        kind: 'CacheJdbcPojoStoreFactory',
+                        CacheJdbcPojoStoreFactory: {
+                            dataSourceBean: toJavaName('ds', cache.name),
+                            dialect: 'Generic'
+                        },
+                        CacheJdbcBlobStoreFactory: {connectVia: 'DataSource'}
+                    },
+                    readThrough: dflt || cache.readThrough,
+                    writeThrough: dflt || cache.writeThrough
+                };
+            }
+
+            return {};
+        },
+        randomString(len) {
+            const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+            const possibleLen = possible.length;
+
+            let res = '';
+
+            for (let i = 0; i < len; i++)
+                res += possible.charAt(Math.floor(Math.random() * possibleLen));
+
+            return res;
+        },
+        checkFieldValidators(ui) {
+            const form = ui.inputForm;
+            const errors = form.$error;
+            const errKeys = Object.keys(errors);
+
+            if (errKeys && errKeys.length > 0) {
+                const firstErrorKey = errKeys[0];
+
+                const firstError = errors[firstErrorKey][0];
+
+                const err = firstError.$error[firstErrorKey];
+
+                const actualError = _.isArray(err) ? err[0] : firstError;
+
+                const errNameFull = actualError.$name;
+                const errNameShort = errNameFull.endsWith('TextInput') ? errNameFull.substring(0, errNameFull.length - 9) : errNameFull;
+
+                const extractErrorMessage = (errName) => {
+                    try {
+                        return errors[firstErrorKey][0].$errorMessages[errName][firstErrorKey];
+                    }
+                    catch (ignored1) {
+                        try {
+                            return form[firstError.$name].$errorMessages[errName][firstErrorKey];
+                        }
+                        catch (ignored2) {
+                            try {
+                                return form.$errorMessages[errName][firstErrorKey];
+                            }
+                            catch (ignored3) {
+                                return false;
+                            }
+                        }
+                    }
+                };
+
+                const msg = extractErrorMessage(errNameFull) || extractErrorMessage(errNameShort) || 'Invalid value!';
+
+                return ErrorPopover.show(errNameFull, msg, ui, firstError.$name);
+            }
+
+            return true;
+        }
+    };
+}
+
+service.$inject = ['IgniteErrorPopover'];
diff --git a/modules/frontend/app/services/Messages.service.js b/modules/frontend/app/services/Messages.service.js
new file mode 100644
index 0000000..05c058b
--- /dev/null
+++ b/modules/frontend/app/services/Messages.service.js
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {CancellationError} from 'app/errors/CancellationError';
+
+/**
+ * Service to show various information and error messages.
+ * @param {mgcrea.ngStrap.alert.IAlertService} $alert
+ * @param {import('./ErrorParser.service').default} errorParser
+ */
+export default function factory($alert, errorParser) {
+    // Common instance of alert modal.
+    let msgModal;
+
+    const errorMessage = (prefix, err) => {
+        return errorParser.extractMessage(err, prefix);
+    };
+
+    const hideAlert = () => {
+        if (msgModal) {
+            msgModal.hide();
+            msgModal.destroy();
+            msgModal = null;
+        }
+    };
+
+    const _showMessage = (message, err, type, duration) => {
+        hideAlert();
+
+        const title = err ? errorMessage(message, err) : errorMessage(null, message);
+
+        msgModal = $alert({type, title, duration});
+
+        msgModal.$scope.icon = `icon-${type}`;
+    };
+
+    return {
+        errorMessage,
+        hideAlert,
+        /**
+         * @param {string|CancellationError} message
+         * @param [err]
+         */
+        showError(message, err, duration = 10) {
+            if (message instanceof CancellationError)
+                return false;
+
+            _showMessage(message, err, 'danger', duration);
+
+            return false;
+        },
+        /**
+         * @param {string} message
+         */
+        showInfo(message, duration = 5) {
+            _showMessage(message, null, 'success', duration);
+        }
+    };
+}
+
+factory.$inject = ['$alert', 'IgniteErrorParser'];
diff --git a/modules/frontend/app/services/ModelNormalizer.service.js b/modules/frontend/app/services/ModelNormalizer.service.js
new file mode 100644
index 0000000..fbbff9b
--- /dev/null
+++ b/modules/frontend/app/services/ModelNormalizer.service.js
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+// Service to normalize objects for dirty checks.
+export default function() {
+    /**
+     * Normalize object for dirty checks.
+     *
+     * @param original
+     * @param dest
+     * @returns {*}
+     */
+    const normalize = (original, dest) => {
+        if (_.isUndefined(original))
+            return dest;
+
+        if (_.isObject(original)) {
+            _.forOwn(original, (value, key) => {
+                if (/\$\$hashKey/.test(key))
+                    return;
+
+                const attr = normalize(value);
+
+                if (!_.isNil(attr)) {
+                    dest = dest || {};
+                    dest[key] = attr;
+                }
+            });
+        } else if (_.isBoolean(original) && original === true)
+            dest = original;
+        else if ((_.isString(original) && original.length) || _.isNumber(original))
+            dest = original;
+        else if (_.isArray(original) && original.length)
+            dest = _.map(original, (value) => normalize(value, {}));
+
+        return dest;
+    };
+
+    return {
+        normalize,
+        isEqual(prev, cur) {
+            return _.isEqual(prev, normalize(cur));
+        }
+    };
+}
diff --git a/modules/frontend/app/services/SqlTypes.service.js b/modules/frontend/app/services/SqlTypes.service.js
new file mode 100644
index 0000000..851c54e
--- /dev/null
+++ b/modules/frontend/app/services/SqlTypes.service.js
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+// List of H2 reserved SQL keywords.
+import H2_SQL_KEYWORDS from 'app/data/sql-keywords.json';
+// List of JDBC type descriptors.
+import JDBC_TYPES from 'app/data/jdbc-types.json';
+
+// Regular expression to check H2 SQL identifier.
+const VALID_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_$]*$/im;
+
+// Descriptor for unknown JDBC type.
+const UNKNOWN_JDBC_TYPE = {
+    dbName: 'Unknown',
+    signed: {javaType: 'Unknown', primitiveType: 'Unknown'},
+    unsigned: {javaType: 'Unknown', primitiveType: 'Unknown'}
+};
+
+/**
+ * Utility service for various check on SQL types.
+ */
+export default class SqlTypes {
+    /**
+     * @param {String} value Value to check.
+     * @returns {boolean} 'true' if given text is valid Java class name.
+     */
+    validIdentifier(value) {
+        return !!(value && VALID_IDENTIFIER.test(value));
+    }
+
+    /**
+     * @param value {String} Value to check.
+     * @returns {boolean} 'true' if given text is one of H2 reserved keywords.
+     */
+    isKeyword(value) {
+        return !!(value && _.includes(H2_SQL_KEYWORDS, value.toUpperCase()));
+    }
+
+    /**
+     * Find JDBC type descriptor for specified JDBC type and options.
+     *
+     * @param {Number} dbType  Column db type.
+     * @return {String} Java type.
+     */
+    findJdbcType(dbType) {
+        const jdbcType = _.find(JDBC_TYPES, (item) => item.dbType === dbType);
+
+        return jdbcType ? jdbcType : UNKNOWN_JDBC_TYPE;
+    }
+}
diff --git a/modules/frontend/app/services/Version.service.js b/modules/frontend/app/services/Version.service.js
new file mode 100644
index 0000000..cbd4b25
--- /dev/null
+++ b/modules/frontend/app/services/Version.service.js
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {BehaviorSubject} from 'rxjs';
+import _ from 'lodash';
+
+/**
+ * Utility service for version parsing and comparing
+ */
+const VERSION_MATCHER = /(\d+)\.(\d+)\.(\d+)([-.]([^0123456789][^-]+)(-SNAPSHOT)?)?(-(\d+))?(-([\da-f]+))?/i;
+
+const numberComparator = (a, b) => a > b ? 1 : a < b ? -1 : 0;
+
+export default class IgniteVersion {
+    constructor() {
+        this.webConsole = '2.8.0';
+
+        this.supportedVersions = [
+            {
+                label: 'Ignite 2.8',
+                ignite: '2.8.0'
+            },
+            {
+                label: 'Ignite 2.7',
+                ignite: '2.7.0'
+            },
+            {
+                label: 'Ignite 2.6',
+                ignite: '2.6.0'
+            },
+            {
+                label: 'Ignite 2.5',
+                ignite: '2.5.0'
+            },
+            {
+                label: 'Ignite 2.4',
+                ignite: '2.4.0'
+            },
+            {
+                label: 'Ignite 2.3',
+                ignite: '2.3.0'
+            },
+            {
+                label: 'Ignite 2.1',
+                ignite: '2.2.0'
+            },
+            {
+                label: 'Ignite 2.0',
+                ignite: '2.0.0'
+            },
+            {
+                label: 'Ignite 1.x',
+                ignite: '1.9.0'
+            }
+        ];
+
+        /** Current product version. */
+        let current = _.head(this.supportedVersions);
+
+        try {
+            const ignite = localStorage.configurationVersion;
+
+            const restored = _.find(this.supportedVersions, {ignite});
+
+            if (restored)
+                current = restored;
+        }
+        catch (ignored) {
+            // No-op.
+        }
+
+        this.currentSbj = new BehaviorSubject(current);
+
+        this.currentSbj.subscribe({
+            next: (ver) => {
+                try {
+                    localStorage.setItem('configurationVersion', ver.ignite);
+                }
+                catch (ignored) {
+                    // No-op.
+                }
+            }
+        });
+    }
+
+    /**
+     * @return {String} Current Ignite version.
+     */
+    get current() {
+        return this.currentSbj.getValue().ignite;
+    }
+
+    /**
+     * Check if version in range.
+     *
+     * @param {String} target Target version.
+     * @param {String | Array.<String>} ranges Version ranges to compare with.
+     * @returns {Boolean} `True` if version is equal or greater than specified range.
+     */
+    since(target, ...ranges) {
+        const targetVer = this.parse(target);
+
+        return !!_.find(ranges, (range) => {
+            if (_.isArray(range)) {
+                const [after, before] = range;
+
+                return this.compare(targetVer, this.parse(after)) >= 0 &&
+                    (_.isNil(before) || this.compare(targetVer, this.parse(before)) < 0);
+            }
+
+            return this.compare(targetVer, this.parse(range)) >= 0;
+        });
+    }
+
+    /**
+     * Check whether version before than specified version.
+     *
+     * @param {String} target Target version.
+     * @param {String} ranges Version ranges to compare with.
+     * @return {Boolean} `True` if version before than specified version.
+     */
+    before(target, ...ranges) {
+        return !this.since(target, ...ranges);
+    }
+
+    /**
+     * Check if current version in specified range.
+     *
+     * @param {String|Array.<String>} ranges Version ranges to compare with.
+     * @returns {Boolean} `True` if configuration version is equal or greater than specified range.
+     */
+    available(...ranges) {
+        return this.since(this.current, ...ranges);
+    }
+
+    /**
+     * Tries to parse product version from it's string representation.
+     *
+     * @param {String} ver - String representation of version.
+     * @returns {{major: Number, minor: Number, maintenance: Number, stage: String, revTs: Number, revHash: String}} - Object that contains product version fields.
+     */
+    parse(ver) {
+        // Development or built from source ZIP.
+        ver = ver.replace(/(-DEV|-n\/a)$/i, '');
+
+        const [, major, minor, maintenance, stage, ...chunks] = ver.match(VERSION_MATCHER);
+
+        return {
+            major: parseInt(major, 10),
+            minor: parseInt(minor, 10),
+            maintenance: parseInt(maintenance, 10),
+            stage: (stage || '').substring(1),
+            revTs: chunks[2] ? parseInt(chunks[3], 10) : 0,
+            revHash: chunks[4] ? chunks[5] : null
+        };
+    }
+
+    /**
+     * Compare to version.
+     *
+     * @param {Object} a first compared version.
+     * @param {Object} b second compared version.
+     * @returns {Number} 1 if a > b, 0 if versions equals, -1 if a < b
+     */
+    compare(a, b) {
+        let res = numberComparator(a.major, b.major);
+
+        if (res !== 0)
+            return res;
+
+        res = numberComparator(a.minor, b.minor);
+
+        if (res !== 0)
+            return res;
+
+        res = numberComparator(a.maintenance, b.maintenance);
+
+        if (res !== 0)
+            return res;
+
+        return numberComparator(a.stage, b.stage);
+    }
+
+}
diff --git a/modules/frontend/app/services/Version.spec.js b/modules/frontend/app/services/Version.spec.js
new file mode 100644
index 0000000..14a2588
--- /dev/null
+++ b/modules/frontend/app/services/Version.spec.js
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import VersionService from './Version.service';
+import {suite, test} from 'mocha';
+import {assert} from 'chai';
+
+const INSTANCE = new VersionService();
+
+suite('VersionServiceTestsSuite', () => {
+    test('Parse 1.7.0-SNAPSHOT', () => {
+        const version = INSTANCE.parse('1.7.0-SNAPSHOT');
+        assert.equal(version.major, 1);
+        assert.equal(version.minor, 7);
+        assert.equal(version.maintenance, 0);
+        assert.equal(version.stage, 'SNAPSHOT');
+        assert.equal(version.revTs, 0);
+        assert.isNull(version.revHash);
+    });
+
+    test('Parse strip -DEV 1.7.0-DEV', () => {
+        const version = INSTANCE.parse('1.7.0-DEV');
+        assert.equal(version.major, 1);
+        assert.equal(version.minor, 7);
+        assert.equal(version.maintenance, 0);
+        assert.equal(version.stage, '');
+    });
+
+    test('Parse strip -n/a 1.7.0-n/a', () => {
+        const version = INSTANCE.parse('1.7.0-n/a');
+        assert.equal(version.major, 1);
+        assert.equal(version.minor, 7);
+        assert.equal(version.maintenance, 0);
+        assert.equal(version.stage, '');
+    });
+
+    test('Check patch version', () => {
+        assert.equal(INSTANCE.compare(INSTANCE.parse('1.7.2'), INSTANCE.parse('1.7.1')), 1);
+    });
+
+    test('Check minor version', () => {
+        assert.equal(INSTANCE.compare(INSTANCE.parse('1.8.1'), INSTANCE.parse('1.7.1')), 1);
+    });
+
+    test('Check major version', () => {
+        assert.equal(INSTANCE.compare(INSTANCE.parse('2.7.1'), INSTANCE.parse('1.7.1')), 1);
+    });
+
+    test('Version a > b', () => {
+        assert.equal(INSTANCE.compare(INSTANCE.parse('1.7.0'), INSTANCE.parse('1.5.0')), 1);
+    });
+
+    test('Version a = b', () => {
+        assert.equal(INSTANCE.compare(INSTANCE.parse('1.0.0'), INSTANCE.parse('1.0.0')), 0);
+        assert.equal(INSTANCE.compare(INSTANCE.parse('1.2.0'), INSTANCE.parse('1.2.0')), 0);
+        assert.equal(INSTANCE.compare(INSTANCE.parse('1.2.3'), INSTANCE.parse('1.2.3')), 0);
+
+        assert.equal(INSTANCE.compare(INSTANCE.parse('1.0.0-1'), INSTANCE.parse('1.0.0-1')), 0);
+        assert.equal(INSTANCE.compare(INSTANCE.parse('1.2.0-1'), INSTANCE.parse('1.2.0-1')), 0);
+        assert.equal(INSTANCE.compare(INSTANCE.parse('1.2.3-1'), INSTANCE.parse('1.2.3-1')), 0);
+    });
+
+    test('Version a < b', () => {
+        assert.equal(INSTANCE.compare(INSTANCE.parse('1.5.1'), INSTANCE.parse('1.5.2')), -1);
+    });
+
+    test('Check since call', () => {
+        assert.equal(INSTANCE.since('1.5.0', '1.5.0'), true);
+        assert.equal(INSTANCE.since('1.6.0', '1.5.0'), true);
+        assert.equal(INSTANCE.since('1.5.4', ['1.5.5', '1.6.0'], ['1.6.2']), false);
+        assert.equal(INSTANCE.since('1.5.5', ['1.5.5', '1.6.0'], ['1.6.2']), true);
+        assert.equal(INSTANCE.since('1.5.11', ['1.5.5', '1.6.0'], ['1.6.2']), true);
+        assert.equal(INSTANCE.since('1.6.0', ['1.5.5', '1.6.0'], ['1.6.2']), false);
+        assert.equal(INSTANCE.since('1.6.1', ['1.5.5', '1.6.0'], '1.6.2'), false);
+        assert.equal(INSTANCE.since('1.6.2', ['1.5.5', '1.6.0'], ['1.6.2']), true);
+        assert.equal(INSTANCE.since('1.6.3', ['1.5.5', '1.6.0'], '1.6.2'), true);
+    });
+
+    test('Check wrong since call', () => {
+        assert.equal(INSTANCE.since('1.3.0', '1.5.0'), false);
+    });
+
+    test('Check before call', () => {
+        assert.equal(INSTANCE.before('1.5.0', '1.5.0'), false);
+        assert.equal(INSTANCE.before('1.5.0', '1.6.0'), true);
+    });
+
+    test('Check wrong before call', () => {
+        assert.equal(INSTANCE.before('1.5.0', '1.3.0'), false);
+    });
+});
diff --git a/modules/frontend/app/services/exceptionHandler.js b/modules/frontend/app/services/exceptionHandler.js
new file mode 100644
index 0000000..6b5f4e4
--- /dev/null
+++ b/modules/frontend/app/services/exceptionHandler.js
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {CancellationError} from 'app/errors/CancellationError';
+
+/**
+ * @param {ng.ILogService} $log
+ */
+export function $exceptionHandler($log) {
+    return function(exception, cause) {
+        if (exception instanceof CancellationError)
+            return;
+
+        // From ui-grid
+        if (exception === 'Possibly unhandled rejection: canceled')
+            return;
+
+        $log.error(exception, cause);
+    };
+}
+
+$exceptionHandler.$inject = ['$log'];
diff --git a/modules/frontend/app/services/index.js b/modules/frontend/app/services/index.js
new file mode 100644
index 0000000..55f8d3d
--- /dev/null
+++ b/modules/frontend/app/services/index.js
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+import IgniteVersion from './Version.service';
+import {default as DefaultState} from './DefaultState';
+
+export default angular
+    .module('ignite-console.services', [])
+    .provider('DefaultState', DefaultState)
+    .service('IgniteVersion', IgniteVersion);
diff --git a/modules/frontend/app/services/store.ts b/modules/frontend/app/services/store.ts
new file mode 100644
index 0000000..c9e4e35
--- /dev/null
+++ b/modules/frontend/app/services/store.ts
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {BehaviorSubject, merge, Subject} from 'rxjs';
+import {scan, tap} from 'rxjs/operators';
+
+interface Reducer<State, Actions> {
+    (state: State, action: Actions): State
+}
+
+export class Store<Actions, State> {
+    static $inject = ['$injector'];
+
+    actions$: Subject<Actions>;
+    state$: BehaviorSubject<State>;
+    private _reducers: Array<Reducer<State, Actions>>;
+
+    constructor(private $injector: ng.auto.IInjectorService) {
+        this.$injector = $injector;
+
+        this.actions$ = new Subject();
+        this.state$ = new BehaviorSubject({});
+        this.actions$.pipe(
+            scan((state, action) => this._reducers.reduce((state, reducer) => reducer(state, action), state), void 0),
+            tap((state) => this.state$.next(state))
+        ).subscribe();
+        this._reducers = [(state) => state];
+    }
+
+    dispatch(action: Actions) {
+        this.actions$.next(action);
+    }
+
+    addReducer<T extends keyof State, U = State[T]>(path: T, reducer: Reducer<U, Actions>): void
+    addReducer(reducer: Reducer<State, Actions>): void
+    addReducer(...args) {
+        if (typeof args[0] === 'string') {
+            const [path, reducer] = args;
+            this._reducers = [
+                ...this._reducers,
+                (state = {}, action) => {
+                    const pathState = reducer(state[path], action);
+                    // Don't update root state if child state changes
+                    return state[path] !== pathState ? {...state, [path]: pathState} : state;
+                }
+            ];
+        } else {
+            const [reducer] = args;
+            this._reducers = [...this._reducers, reducer];
+        }
+    }
+
+    addEffects(EffectsClass) {
+        const instance = this.$injector.instantiate(EffectsClass);
+        return merge(
+            ...Object.keys(instance).filter((k) => k.endsWith('Effect$')).map((k) => instance[k]),
+        ).pipe(tap((a) => this.dispatch(a))).subscribe();
+    }
+}
diff --git a/modules/frontend/app/store/actions/ui.ts b/modules/frontend/app/store/actions/ui.ts
new file mode 100644
index 0000000..25d9ba0
--- /dev/null
+++ b/modules/frontend/app/store/actions/ui.ts
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {NavigationMenuItem} from '../../types';
+
+export const TOGGLE_SIDEBAR: 'TOGGLE_SIDEBAR' = 'TOGGLE_SIDEBAR';
+export const toggleSidebar = () => ({type: TOGGLE_SIDEBAR});
+
+export const NAVIGATION_MENU_ITEM: 'NAVIGATION_MENU_ITEM' = 'NAVIGATION_MENU_ITEM';
+export const navigationMenuItem = (menuItem: NavigationMenuItem) => ({type: NAVIGATION_MENU_ITEM, menuItem});
+
+export const HIDE_NAVIGATION_MENU_ITEM: 'HIDE_NAVIGATION_MENU_ITEM' = 'HIDE_NAVIGATION_MENU_ITEM';
+export const hideNavigationMenuItem = (label: NavigationMenuItem['label']) => ({type: HIDE_NAVIGATION_MENU_ITEM, label});
+
+export const SHOW_NAVIGATION_MENU_ITEM: 'SHOW_NAVIGATION_MENU_ITEM' = 'SHOW_NAVIGATION_MENU_ITEM';
+export const showNavigationMenuItem = (label: NavigationMenuItem['label']) => ({type: SHOW_NAVIGATION_MENU_ITEM, label});
+
+export type UIActions =
+    | ReturnType<typeof toggleSidebar>
+    | ReturnType<typeof navigationMenuItem>
+    | ReturnType<typeof hideNavigationMenuItem>
+    | ReturnType<typeof showNavigationMenuItem>;
diff --git a/modules/frontend/app/store/actions/user.ts b/modules/frontend/app/store/actions/user.ts
new file mode 100644
index 0000000..34c8d20
--- /dev/null
+++ b/modules/frontend/app/store/actions/user.ts
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {User} from '../../types';
+
+export const USER: 'USER' = 'USER';
+export const user = (user: User) => ({type: USER, user});
+
+export type UserActions =
+    | ReturnType<typeof user>;
diff --git a/modules/frontend/app/store/effects/ui.ts b/modules/frontend/app/store/effects/ui.ts
new file mode 100644
index 0000000..be5cf5d
--- /dev/null
+++ b/modules/frontend/app/store/effects/ui.ts
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {AppStore, hideNavigationMenuItem, ofType, showNavigationMenuItem, USER} from '..';
+import {map} from 'rxjs/operators';
+
+export class UIEffects {
+    static $inject = ['Store'];
+    constructor(private store: AppStore) {}
+
+    toggleQueriesNavItemEffect$ = this.store.actions$.pipe(
+        ofType(USER),
+        map((action) => {
+            const QUERY_LABEL = 'Queries';
+            return action.user.becomeUsed ? hideNavigationMenuItem(QUERY_LABEL) : showNavigationMenuItem(QUERY_LABEL);
+        })
+    )
+}
diff --git a/modules/frontend/app/store/index.ts b/modules/frontend/app/store/index.ts
new file mode 100644
index 0000000..c07cff5
--- /dev/null
+++ b/modules/frontend/app/store/index.ts
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Store} from '../services/store';
+
+import {uiReducer, UIState} from './reducers/ui';
+import {UIActions} from './actions/ui';
+import {UIEffects} from './effects/ui';
+
+import {UserActions} from './actions/user';
+
+export * from './actions/ui';
+export * from './reducers/ui';
+export * from './selectors/ui';
+
+export * from './actions/user';
+
+export {ofType} from './ofType';
+
+export type State = {
+    ui: UIState,
+};
+
+export type Actions =
+    | UserActions
+    | UIActions;
+
+export type AppStore = Store<Actions, State>;
+
+register.$inject = ['Store'];
+export function register(store: AppStore) {
+    store.addReducer('ui', uiReducer);
+    store.addEffects(UIEffects);
+}
diff --git a/modules/frontend/app/store/ofType.ts b/modules/frontend/app/store/ofType.ts
new file mode 100644
index 0000000..220de5f
--- /dev/null
+++ b/modules/frontend/app/store/ofType.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {OperatorFunction} from 'rxjs';
+import {filter} from 'rxjs/operators';
+
+type Action = {type: string};
+export function ofType<T extends string, U extends Action, V extends Extract<U, {type: T}>>(type: T): OperatorFunction<U, V>
+export function ofType<U extends Action>(type): OperatorFunction<U, U> {
+    return filter((action: U): boolean => type === action.type);
+}
+
diff --git a/modules/frontend/app/store/reducers/ui.ts b/modules/frontend/app/store/reducers/ui.ts
new file mode 100644
index 0000000..11cf9b8
--- /dev/null
+++ b/modules/frontend/app/store/reducers/ui.ts
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {NavigationMenu} from '../../types';
+import {
+    HIDE_NAVIGATION_MENU_ITEM,
+    NAVIGATION_MENU_ITEM,
+    SHOW_NAVIGATION_MENU_ITEM,
+    TOGGLE_SIDEBAR,
+    UIActions
+} from '..';
+
+export type UIState = {
+    sidebarOpened: boolean,
+    navigationMenu: NavigationMenu
+};
+
+const defaults: UIState = {
+    sidebarOpened: false,
+    navigationMenu: []
+};
+
+export function uiReducer(state: UIState = defaults, action: UIActions): UIState {
+    switch (action.type) {
+        case TOGGLE_SIDEBAR:
+            return {...state, sidebarOpened: !state.sidebarOpened};
+        case NAVIGATION_MENU_ITEM:
+            return {...state, navigationMenu: [...state.navigationMenu, action.menuItem]};
+        case HIDE_NAVIGATION_MENU_ITEM:
+            return {
+                ...state,
+                navigationMenu: state.navigationMenu.map((i) => i.label === action.label ? {...i, hidden: true} : i)
+            };
+        case SHOW_NAVIGATION_MENU_ITEM:
+            return {
+                ...state,
+                navigationMenu: state.navigationMenu.map((i) => i.label === action.label ? {...i, hidden: false} : i)
+            };
+        default:
+            return state;
+    }
+}
diff --git a/modules/frontend/app/store/reduxDebug.ts b/modules/frontend/app/store/reduxDebug.ts
new file mode 100644
index 0000000..ea7fc7f
--- /dev/null
+++ b/modules/frontend/app/store/reduxDebug.ts
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {devTools, reducer} from './reduxDevtoolsIntegration';
+import {AppStore} from '.';
+import {filter, tap, withLatestFrom} from 'rxjs/operators';
+
+run.$inject = ['Store'];
+
+export function run(store: AppStore) {
+    if (devTools) {
+        devTools.subscribe((e) => {
+            if (e.type === 'DISPATCH' && e.state) store.dispatch(e);
+        });
+
+        const ignoredActions = new Set([
+        ]);
+
+        store.actions$.pipe(
+            filter((e) => e.type !== 'DISPATCH'),
+            withLatestFrom(store.state$.skip(1)),
+            tap(([action, state]) => {
+                if (ignoredActions.has(action.type)) return;
+                devTools.send(action, state);
+                console.log(action);
+            })
+        ).subscribe();
+
+        store.addReducer(reducer);
+    }
+}
diff --git a/modules/frontend/app/store/reduxDevtoolsIntegration.js b/modules/frontend/app/store/reduxDevtoolsIntegration.js
new file mode 100644
index 0000000..ad45250
--- /dev/null
+++ b/modules/frontend/app/store/reduxDevtoolsIntegration.js
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export let devTools;
+
+const replacer = (key, value) => {
+    if (value instanceof Map) {
+        return {
+            data: [...value.entries()],
+            __serializedType__: 'Map'
+        };
+    }
+    if (value instanceof Set) {
+        return {
+            data: [...value.values()],
+            __serializedType__: 'Set'
+        };
+    }
+    if (value instanceof Symbol) {
+        return {
+            data: String(value),
+            __serializedType__: 'Symbol'
+        };
+    }
+    if (value instanceof Promise) {
+        return {
+            data: {}
+        };
+    }
+    return value;
+};
+
+const reviver = (key, value) => {
+    if (typeof value === 'object' && value !== null && '__serializedType__' in value) {
+        const data = value.data;
+        switch (value.__serializedType__) {
+            case 'Map':
+                return new Map(value.data);
+            case 'Set':
+                return new Set(value.data);
+            default:
+                return data;
+        }
+    }
+    return value;
+};
+
+if (window.__REDUX_DEVTOOLS_EXTENSION__) {
+    devTools = window.__REDUX_DEVTOOLS_EXTENSION__.connect({
+        name: 'Ignite Web Console',
+        serialize: {
+            replacer,
+            reviver
+        }
+    });
+}
+
+export const reducer = (state, action) => {
+    switch (action.type) {
+        case 'DISPATCH':
+        case 'JUMP_TO_STATE':
+            return JSON.parse(action.state, reviver);
+        default:
+            return state;
+    }
+};
diff --git a/modules/frontend/app/store/selectors/ui.ts b/modules/frontend/app/store/selectors/ui.ts
new file mode 100644
index 0000000..9421547
--- /dev/null
+++ b/modules/frontend/app/store/selectors/ui.ts
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {State} from '..';
+import {map, pluck} from 'rxjs/operators';
+import {pipe} from 'rxjs';
+import {orderBy} from 'lodash';
+
+const orderMenu = <T extends {order: number}>(menu: Array<T>) => orderBy(menu, 'order');
+
+export const selectSidebarOpened = () => pluck<State, State['ui']['sidebarOpened']>('ui', 'sidebarOpened');
+export const selectNavigationMenu = () => pipe(
+    pluck<State, State['ui']['navigationMenu']>('ui', 'navigationMenu'),
+    map(orderMenu)
+);
diff --git a/modules/frontend/app/style.scss b/modules/frontend/app/style.scss
new file mode 100644
index 0000000..c7eb726
--- /dev/null
+++ b/modules/frontend/app/style.scss
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.flex-full-height > [ui-view] {
+	display: flex;
+    flex-direction: column;
+    flex: 1 0 auto;
+}
diff --git a/modules/frontend/app/types/index.ts b/modules/frontend/app/types/index.ts
new file mode 100644
index 0000000..905558a
--- /dev/null
+++ b/modules/frontend/app/types/index.ts
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Ng1StateDeclaration} from '@uirouter/angularjs';
+
+interface ITfMetatagsConfig {
+    title: string
+}
+
+export interface IIgniteNg1StateDeclaration extends Ng1StateDeclaration {
+    /**
+     * Whether to store state as last visited in local storage or not:
+     * `true` - will be saved
+     * `false` (default) - won't be saved
+     * @type {boolean}
+     */
+    unsaved?: boolean,
+    tfMetaTags: ITfMetatagsConfig,
+    permission?: string
+}
+
+export type User = {
+    _id: string,
+    firstName: string,
+    lastName: string,
+    email: string,
+    phone?: string,
+    company: string,
+    country: string,
+    registered: string,
+    lastLogin: string,
+    lastActivity: string,
+    admin: boolean,
+    token: string,
+    resetPasswordToken: string,
+    // Assigned in UI
+    becomeUsed?: boolean
+};
+
+export type NavigationMenuItem = {
+    label: string,
+    icon: string,
+    order: number,
+    hidden?: boolean
+} & (
+    {sref: string, activeSref: string} |
+    {href: string}
+);
+
+export type NavigationMenu = Array<NavigationMenuItem>;
+
+export type MenuItem <T> = {
+    label: string,
+    value: T
+};
+
+export type Menu <T> = MenuItem<T>[];
+
+export interface IInputErrorNotifier {
+    notifyAboutError(): void
+    hideError(): void
+}
+
+export enum WellKnownOperationStatus {
+    WAITING = 'WAITING',
+    ERROR = 'ERROR',
+    DONE = 'DONE'
+}
diff --git a/modules/frontend/app/utils/SimpleWorkerPool.js b/modules/frontend/app/utils/SimpleWorkerPool.js
new file mode 100644
index 0000000..ab563bb
--- /dev/null
+++ b/modules/frontend/app/utils/SimpleWorkerPool.js
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+import {race, Subject} from 'rxjs';
+import {filter, map, pluck, take} from 'rxjs/operators';
+
+/**
+ * Simple implementation of workers pool.
+ */
+export default class SimpleWorkerPool {
+    constructor(name, WorkerClass, poolSize = (navigator.hardwareConcurrency || 4), dbg = false) {
+        this._name = name;
+        this._WorkerClass = WorkerClass;
+        this._tasks = [];
+        this._msgId = 0;
+        this.messages$ = new Subject();
+        this.errors$ = new Subject();
+        this.__dbg = dbg;
+
+        this._workers = _.range(poolSize).map(() => {
+            const worker = new this._WorkerClass();
+
+            worker.onmessage = (m) => {
+                this.messages$.next({tid: worker.tid, m});
+
+                worker.tid = null;
+
+                this._run();
+            };
+
+            worker.onerror = (e) => {
+                this.errors$.next({tid: worker.tid, e});
+
+                worker.tid = null;
+
+                this._run();
+            };
+
+            return worker;
+        });
+    }
+
+    _makeTaskID() {
+        return this._msgId++;
+    }
+
+    _getNextWorker() {
+        return this._workers.find((w) => _.isNil(w.tid));
+    }
+
+    _getNextTask() {
+        return this._tasks.shift();
+    }
+
+    _run() {
+        const worker = this._getNextWorker();
+
+        if (!worker || !this._tasks.length)
+            return;
+
+        const task = this._getNextTask();
+
+        worker.tid = task.tid;
+
+        if (this.__dbg)
+            console.time(`Post message[pool=${this._name}]`);
+
+        worker.postMessage(task.data);
+
+        if (this.__dbg)
+            console.timeEnd(`Post message[pool=${this._name}]`);
+    }
+
+    terminate() {
+        this._workers.forEach((w) => w.terminate());
+
+        this.messages$.complete();
+        this.errors$.complete();
+
+        this._workers = null;
+    }
+
+    postMessage(data) {
+        const tid = this._makeTaskID();
+
+        this._tasks.push({tid, data});
+
+        if (this.__dbg)
+            console.log(`Pool: [name=${this._name}, tid=${tid}, queue=${this._tasks.length}]`);
+
+        this._run();
+
+        return race(
+            this.messages$.pipe(filter((e) => e.tid === tid), take(1), pluck('m', 'data')),
+            this.errors$.pipe(filter((e) => e.tid === tid), take(1), map((e) => {
+                throw e.e;
+            }))
+        ).pipe(take(1)).toPromise();
+    }
+}
diff --git a/modules/frontend/app/utils/dialogState.ts b/modules/frontend/app/utils/dialogState.ts
new file mode 100644
index 0000000..abadfdf
--- /dev/null
+++ b/modules/frontend/app/utils/dialogState.ts
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {StateDeclaration, Transition, UIRouter} from '@uirouter/angularjs';
+
+export function dialogState(component: string): Partial<StateDeclaration> {
+    let dialog: mgcrea.ngStrap.modal.IModal | undefined;
+    let hide: (() => void) | undefined;
+
+    onEnter.$inject = ['$transition$'];
+
+    function onEnter(transition: Transition) {
+        const modal = transition.injector().get<mgcrea.ngStrap.modal.IModalService>('$modal');
+        const router = transition.injector().get<UIRouter>('$uiRouter');
+
+        dialog = modal({
+            template: `
+                <${component}
+                    class='modal center modal--ignite theme--ignite'
+                    tabindex='-1'
+                    role='dialog'
+                    on-hide=$hide()
+                ></${component}>
+            `,
+            onHide(modal) {
+                modal.destroy();
+            }
+        });
+
+        hide = dialog.hide;
+
+        dialog.hide = () => router.stateService.go('.^');
+    }
+
+    return {
+        onEnter,
+        onExit() {
+            if (hide) hide();
+            dialog = hide = void 0;
+        }
+    };
+}
diff --git a/modules/frontend/app/utils/id8.js b/modules/frontend/app/utils/id8.js
new file mode 100644
index 0000000..c6bbeb6
--- /dev/null
+++ b/modules/frontend/app/utils/id8.js
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default function id8(uuid = '') {
+    return uuid.substring(0, 8).toUpperCase();
+}
diff --git a/modules/frontend/app/utils/lodashMixins.js b/modules/frontend/app/utils/lodashMixins.js
new file mode 100644
index 0000000..ff50ee0
--- /dev/null
+++ b/modules/frontend/app/utils/lodashMixins.js
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import negate from 'lodash/negate';
+import isNil from 'lodash/isNil';
+import isEmpty from 'lodash/isEmpty';
+
+export const nonNil = negate(isNil);
+export const nonEmpty = negate(isEmpty);
diff --git a/modules/frontend/app/utils/uniqueName.js b/modules/frontend/app/utils/uniqueName.js
new file mode 100644
index 0000000..bebe2c3
--- /dev/null
+++ b/modules/frontend/app/utils/uniqueName.js
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const uniqueName = (name, items, fn = ({name, i}) => `${name}${i}`) => {
+    let i = 0;
+    let newName = name;
+    const isUnique = (item) => item.name === newName;
+    while (items.some(isUnique)) {
+        i += 1;
+        newName = fn({name, i});
+    }
+    return newName;
+};
diff --git a/modules/frontend/app/vendor.js b/modules/frontend/app/vendor.js
new file mode 100644
index 0000000..2800739
--- /dev/null
+++ b/modules/frontend/app/vendor.js
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'jquery';
+import 'angular';
+import 'angular-acl';
+import 'angular-animate';
+import 'angular-sanitize';
+import 'angular-strap';
+import 'angular-strap/dist/angular-strap.tpl';
+import 'angular1-async-filter';
+
+import 'angular-messages';
+import '@uirouter/angularjs';
+
+import 'resize-observer-polyfill';
+
+import 'tf-metatags';
+import 'angular-translate';
+import 'angular-smart-table';
+import 'angular-ui-grid/ui-grid';
+import 'angular-drag-and-drop-lists';
+import 'angular-nvd3';
+import 'angular-tree-control';
+import 'angular-gridster';
+import 'brace';
+import 'brace/mode/xml';
+import 'brace/mode/sql';
+import 'brace/mode/java';
+import 'brace/mode/csharp';
+import 'brace/mode/dockerfile';
+import 'brace/mode/snippets';
+import 'brace/theme/chrome';
+import 'brace/ext/language_tools';
+import 'brace/ext/searchbox';
+import 'file-saver';
+import 'jszip';
+import 'nvd3';
+import 'lodash';
+
+import 'angular-gridster/dist/angular-gridster.min.css';
+import 'angular-tree-control/css/tree-control-attribute.css';
+import 'angular-tree-control/css/tree-control.css';
+import 'angular-ui-grid/ui-grid.css';
+import 'nvd3/build/nv.d3.css';
diff --git a/modules/frontend/index.js b/modules/frontend/index.js
new file mode 100644
index 0000000..bf725ac
--- /dev/null
+++ b/modules/frontend/index.js
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import angular from 'angular';
+
+import igniteConsole from './app/app';
+import configurationLazyModule from './app/configuration/index.lazy';
+
+angular.bootstrap(document, [igniteConsole.name, configurationLazyModule.name], {strictDi: true});
diff --git a/modules/frontend/package-lock.json b/modules/frontend/package-lock.json
new file mode 100644
index 0000000..459e517
--- /dev/null
+++ b/modules/frontend/package-lock.json
@@ -0,0 +1,14224 @@
+{
+  "name": "ignite-web-console",
+  "version": "2.7.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@babel/code-frame": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
+      "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
+      "requires": {
+        "@babel/highlight": "^7.0.0"
+      }
+    },
+    "@babel/core": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.0.1.tgz",
+      "integrity": "sha512-7Yy2vRB6KYbhWeIrrwJmKv9UwDxokmlo43wi6AV84oNs4Gi71NTNGh3YxY/hK3+CxuSc6wcKSl25F2tQOhm1GQ==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "@babel/generator": "^7.0.0",
+        "@babel/helpers": "^7.0.0",
+        "@babel/parser": "^7.0.0",
+        "@babel/template": "^7.0.0",
+        "@babel/traverse": "^7.0.0",
+        "@babel/types": "^7.0.0",
+        "convert-source-map": "^1.1.0",
+        "debug": "^3.1.0",
+        "json5": "^0.5.0",
+        "lodash": "^4.17.10",
+        "resolve": "^1.3.2",
+        "semver": "^5.4.1",
+        "source-map": "^0.5.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
+      }
+    },
+    "@babel/generator": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.2.2.tgz",
+      "integrity": "sha512-I4o675J/iS8k+P38dvJ3IBGqObLXyQLTxtrR4u9cSUJOURvafeEWb/pFMOTwtNrmq73mJzyF6ueTbO1BtN0Zeg==",
+      "requires": {
+        "@babel/types": "^7.2.2",
+        "jsesc": "^2.5.1",
+        "lodash": "^4.17.10",
+        "source-map": "^0.5.0",
+        "trim-right": "^1.0.1"
+      }
+    },
+    "@babel/helper-annotate-as-pure": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz",
+      "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-builder-binary-assignment-operator-visitor": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz",
+      "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-explode-assignable-expression": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-call-delegate": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.1.0.tgz",
+      "integrity": "sha512-YEtYZrw3GUK6emQHKthltKNZwszBcHK58Ygcis+gVUrF4/FmTVr5CCqQNSfmvg2y+YDEANyYoaLz/SHsnusCwQ==",
+      "requires": {
+        "@babel/helper-hoist-variables": "^7.0.0",
+        "@babel/traverse": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-define-map": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz",
+      "integrity": "sha512-yPPcW8dc3gZLN+U1mhYV91QU3n5uTbx7DUdf8NnPbjS0RMwBuHi9Xt2MUgppmNz7CJxTBWsGczTiEp1CSOTPRg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-function-name": "^7.1.0",
+        "@babel/types": "^7.0.0",
+        "lodash": "^4.17.10"
+      }
+    },
+    "@babel/helper-explode-assignable-expression": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz",
+      "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==",
+      "dev": true,
+      "requires": {
+        "@babel/traverse": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-function-name": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz",
+      "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==",
+      "requires": {
+        "@babel/helper-get-function-arity": "^7.0.0",
+        "@babel/template": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-get-function-arity": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz",
+      "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==",
+      "requires": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-hoist-variables": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0.tgz",
+      "integrity": "sha512-Ggv5sldXUeSKsuzLkddtyhyHe2YantsxWKNi7A+7LeD12ExRDWTRk29JCXpaHPAbMaIPZSil7n+lq78WY2VY7w==",
+      "requires": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-member-expression-to-functions": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz",
+      "integrity": "sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-module-imports": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz",
+      "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-module-transforms": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.2.2.tgz",
+      "integrity": "sha512-YRD7I6Wsv+IHuTPkAmAS4HhY0dkPobgLftHp0cRGZSdrRvmZY8rFvae/GVu3bD00qscuvK3WPHB3YdNpBXUqrA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-module-imports": "^7.0.0",
+        "@babel/helper-simple-access": "^7.1.0",
+        "@babel/helper-split-export-declaration": "^7.0.0",
+        "@babel/template": "^7.2.2",
+        "@babel/types": "^7.2.2",
+        "lodash": "^4.17.10"
+      }
+    },
+    "@babel/helper-optimise-call-expression": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz",
+      "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==",
+      "dev": true,
+      "requires": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-plugin-utils": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz",
+      "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA=="
+    },
+    "@babel/helper-regex": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.0.0.tgz",
+      "integrity": "sha512-TR0/N0NDCcUIUEbqV6dCO+LptmmSQFQ7q70lfcEB4URsjD0E1HzicrwUH+ap6BAQ2jhCX9Q4UqZy4wilujWlkg==",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.17.10"
+      }
+    },
+    "@babel/helper-remap-async-to-generator": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz",
+      "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-annotate-as-pure": "^7.0.0",
+        "@babel/helper-wrap-function": "^7.1.0",
+        "@babel/template": "^7.1.0",
+        "@babel/traverse": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-replace-supers": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.2.3.tgz",
+      "integrity": "sha512-GyieIznGUfPXPWu0yLS6U55Mz67AZD9cUk0BfirOWlPrXlBcan9Gz+vHGz+cPfuoweZSnPzPIm67VtQM0OWZbA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-member-expression-to-functions": "^7.0.0",
+        "@babel/helper-optimise-call-expression": "^7.0.0",
+        "@babel/traverse": "^7.2.3",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-simple-access": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz",
+      "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==",
+      "dev": true,
+      "requires": {
+        "@babel/template": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-split-export-declaration": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz",
+      "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==",
+      "requires": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "@babel/helper-wrap-function": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz",
+      "integrity": "sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-function-name": "^7.1.0",
+        "@babel/template": "^7.1.0",
+        "@babel/traverse": "^7.1.0",
+        "@babel/types": "^7.2.0"
+      }
+    },
+    "@babel/helpers": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.2.0.tgz",
+      "integrity": "sha512-Fr07N+ea0dMcMN8nFpuK6dUIT7/ivt9yKQdEEnjVS83tG2pHwPi03gYmk/tyuwONnZ+sY+GFFPlWGgCtW1hF9A==",
+      "dev": true,
+      "requires": {
+        "@babel/template": "^7.1.2",
+        "@babel/traverse": "^7.1.5",
+        "@babel/types": "^7.2.0"
+      }
+    },
+    "@babel/highlight": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
+      "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
+      "requires": {
+        "chalk": "^2.0.0",
+        "esutils": "^2.0.2",
+        "js-tokens": "^4.0.0"
+      }
+    },
+    "@babel/parser": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.2.3.tgz",
+      "integrity": "sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA=="
+    },
+    "@babel/plugin-proposal-async-generator-functions": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz",
+      "integrity": "sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/helper-remap-async-to-generator": "^7.1.0",
+        "@babel/plugin-syntax-async-generators": "^7.2.0"
+      }
+    },
+    "@babel/plugin-proposal-class-properties": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.0.0.tgz",
+      "integrity": "sha512-mVgsbdySh6kuzv4omXvw0Kuh+3hrUrQ883qTCf75MqfC6zctx2LXrP3Wt+bbJmB5fE5nfhf/Et2pQyrRy4j0Pg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-function-name": "^7.0.0",
+        "@babel/helper-member-expression-to-functions": "^7.0.0",
+        "@babel/helper-optimise-call-expression": "^7.0.0",
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/helper-replace-supers": "^7.0.0",
+        "@babel/plugin-syntax-class-properties": "^7.0.0"
+      }
+    },
+    "@babel/plugin-proposal-json-strings": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz",
+      "integrity": "sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/plugin-syntax-json-strings": "^7.2.0"
+      }
+    },
+    "@babel/plugin-proposal-object-rest-spread": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.0.0.tgz",
+      "integrity": "sha512-14fhfoPcNu7itSen7Py1iGN0gEm87hX/B+8nZPqkdmANyyYWYMY2pjA3r8WXbWVKMzfnSNS0xY8GVS0IjXi/iw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/plugin-syntax-object-rest-spread": "^7.0.0"
+      }
+    },
+    "@babel/plugin-proposal-optional-catch-binding": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz",
+      "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/plugin-syntax-optional-catch-binding": "^7.2.0"
+      }
+    },
+    "@babel/plugin-proposal-unicode-property-regex": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz",
+      "integrity": "sha512-LvRVYb7kikuOtIoUeWTkOxQEV1kYvL5B6U3iWEGCzPNRus1MzJweFqORTj+0jkxozkTSYNJozPOddxmqdqsRpw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/helper-regex": "^7.0.0",
+        "regexpu-core": "^4.2.0"
+      }
+    },
+    "@babel/plugin-syntax-async-generators": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz",
+      "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-syntax-class-properties": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.2.0.tgz",
+      "integrity": "sha512-UxYaGXYQ7rrKJS/PxIKRkv3exi05oH7rokBAsmCSsCxz1sVPZ7Fu6FzKoGgUvmY+0YgSkYHgUoCh5R5bCNBQlw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-syntax-dynamic-import": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.0.0.tgz",
+      "integrity": "sha512-Gt9xNyRrCHCiyX/ZxDGOcBnlJl0I3IWicpZRC4CdC0P5a/I07Ya2OAMEBU+J7GmRFVmIetqEYRko6QYRuKOESw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-syntax-json-strings": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz",
+      "integrity": "sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-syntax-object-rest-spread": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz",
+      "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-syntax-optional-catch-binding": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz",
+      "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-syntax-typescript": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.2.0.tgz",
+      "integrity": "sha512-WhKr6yu6yGpGcNMVgIBuI9MkredpVc7Y3YR4UzEZmDztHoL6wV56YBHLhWnjO1EvId1B32HrD3DRFc+zSoKI1g==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-arrow-functions": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz",
+      "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-async-to-generator": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.2.0.tgz",
+      "integrity": "sha512-CEHzg4g5UraReozI9D4fblBYABs7IM6UerAVG7EJVrTLC5keh00aEuLUT+O40+mJCEzaXkYfTCUKIyeDfMOFFQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-module-imports": "^7.0.0",
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/helper-remap-async-to-generator": "^7.1.0"
+      }
+    },
+    "@babel/plugin-transform-block-scoped-functions": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz",
+      "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-block-scoping": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.2.0.tgz",
+      "integrity": "sha512-vDTgf19ZEV6mx35yiPJe4fS02mPQUUcBNwWQSZFXSzTSbsJFQvHt7DqyS3LK8oOWALFOsJ+8bbqBgkirZteD5Q==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "lodash": "^4.17.10"
+      }
+    },
+    "@babel/plugin-transform-classes": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.2.2.tgz",
+      "integrity": "sha512-gEZvgTy1VtcDOaQty1l10T3jQmJKlNVxLDCs+3rCVPr6nMkODLELxViq5X9l+rfxbie3XrfrMCYYY6eX3aOcOQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-annotate-as-pure": "^7.0.0",
+        "@babel/helper-define-map": "^7.1.0",
+        "@babel/helper-function-name": "^7.1.0",
+        "@babel/helper-optimise-call-expression": "^7.0.0",
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/helper-replace-supers": "^7.1.0",
+        "@babel/helper-split-export-declaration": "^7.0.0",
+        "globals": "^11.1.0"
+      }
+    },
+    "@babel/plugin-transform-computed-properties": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz",
+      "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-destructuring": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.2.0.tgz",
+      "integrity": "sha512-coVO2Ayv7g0qdDbrNiadE4bU7lvCd9H539m2gMknyVjjMdwF/iCOM7R+E8PkntoqLkltO0rk+3axhpp/0v68VQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-dotall-regex": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.2.0.tgz",
+      "integrity": "sha512-sKxnyHfizweTgKZf7XsXu/CNupKhzijptfTM+bozonIuyVrLWVUvYjE2bhuSBML8VQeMxq4Mm63Q9qvcvUcciQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/helper-regex": "^7.0.0",
+        "regexpu-core": "^4.1.3"
+      }
+    },
+    "@babel/plugin-transform-duplicate-keys": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz",
+      "integrity": "sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-exponentiation-operator": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz",
+      "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0",
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-for-of": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.2.0.tgz",
+      "integrity": "sha512-Kz7Mt0SsV2tQk6jG5bBv5phVbkd0gd27SgYD4hH1aLMJRchM0dzHaXvrWhVZ+WxAlDoAKZ7Uy3jVTW2mKXQ1WQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-function-name": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.2.0.tgz",
+      "integrity": "sha512-kWgksow9lHdvBC2Z4mxTsvc7YdY7w/V6B2vy9cTIPtLEE9NhwoWivaxdNM/S37elu5bqlLP/qOY906LukO9lkQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-function-name": "^7.1.0",
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-literals": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz",
+      "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-modules-amd": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.2.0.tgz",
+      "integrity": "sha512-mK2A8ucqz1qhrdqjS9VMIDfIvvT2thrEsIQzbaTdc5QFzhDjQv2CkJJ5f6BXIkgbmaoax3zBr2RyvV/8zeoUZw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-module-transforms": "^7.1.0",
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-modules-commonjs": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.2.0.tgz",
+      "integrity": "sha512-V6y0uaUQrQPXUrmj+hgnks8va2L0zcZymeU7TtWEgdRLNkceafKXEduv7QzgQAE4lT+suwooG9dC7LFhdRAbVQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-module-transforms": "^7.1.0",
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/helper-simple-access": "^7.1.0"
+      }
+    },
+    "@babel/plugin-transform-modules-systemjs": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.2.0.tgz",
+      "integrity": "sha512-aYJwpAhoK9a+1+O625WIjvMY11wkB/ok0WClVwmeo3mCjcNRjt+/8gHWrB5i+00mUju0gWsBkQnPpdvQ7PImmQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-hoist-variables": "^7.0.0",
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-modules-umd": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz",
+      "integrity": "sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-module-transforms": "^7.1.0",
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-new-target": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz",
+      "integrity": "sha512-yin069FYjah+LbqfGeTfzIBODex/e++Yfa0rH0fpfam9uTbuEeEOx5GLGr210ggOV77mVRNoeqSYqeuaqSzVSw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-object-super": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz",
+      "integrity": "sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/helper-replace-supers": "^7.1.0"
+      }
+    },
+    "@babel/plugin-transform-parameters": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.0.0.tgz",
+      "integrity": "sha512-eWngvRBWx0gScot0xa340JzrkA+8HGAk1OaCHDfXAjkrTFkp73Lcf+78s7AStSdRML5nzx5aXpnjN1MfrjkBoA==",
+      "requires": {
+        "@babel/helper-call-delegate": "^7.0.0",
+        "@babel/helper-get-function-arity": "^7.0.0",
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-regenerator": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0.tgz",
+      "integrity": "sha512-sj2qzsEx8KDVv1QuJc/dEfilkg3RRPvPYx/VnKLtItVQRWt1Wqf5eVCOLZm29CiGFfYYsA3VPjfizTCV0S0Dlw==",
+      "dev": true,
+      "requires": {
+        "regenerator-transform": "^0.13.3"
+      }
+    },
+    "@babel/plugin-transform-shorthand-properties": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz",
+      "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-spread": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz",
+      "integrity": "sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-sticky-regex": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz",
+      "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/helper-regex": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-template-literals": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.2.0.tgz",
+      "integrity": "sha512-FkPix00J9A/XWXv4VoKJBMeSkyY9x/TqIh76wzcdfl57RJJcf8CehQ08uwfhCDNtRQYtHQKBTwKZDEyjE13Lwg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-annotate-as-pure": "^7.0.0",
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-typeof-symbol": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz",
+      "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0"
+      }
+    },
+    "@babel/plugin-transform-typescript": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.2.0.tgz",
+      "integrity": "sha512-EnI7i2/gJ7ZNr2MuyvN2Hu+BHJENlxWte5XygPvfj/MbvtOkWor9zcnHpMMQL2YYaaCcqtIvJUyJ7QVfoGs7ew==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/plugin-syntax-typescript": "^7.2.0"
+      }
+    },
+    "@babel/plugin-transform-unicode-regex": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.2.0.tgz",
+      "integrity": "sha512-m48Y0lMhrbXEJnVUaYly29jRXbQ3ksxPrS1Tg8t+MHqzXhtBYAvI51euOBaoAlZLPHsieY9XPVMf80a5x0cPcA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/helper-regex": "^7.0.0",
+        "regexpu-core": "^4.1.3"
+      }
+    },
+    "@babel/preset-env": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.0.0.tgz",
+      "integrity": "sha512-Fnx1wWaWv2w2rl+VHxA9si//Da40941IQ29fKiRejVR7oN1FxSEL8+SyAX/2oKIye2gPvY/GBbJVEKQ/oi43zQ==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-module-imports": "^7.0.0",
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/plugin-proposal-async-generator-functions": "^7.0.0",
+        "@babel/plugin-proposal-json-strings": "^7.0.0",
+        "@babel/plugin-proposal-object-rest-spread": "^7.0.0",
+        "@babel/plugin-proposal-optional-catch-binding": "^7.0.0",
+        "@babel/plugin-proposal-unicode-property-regex": "^7.0.0",
+        "@babel/plugin-syntax-async-generators": "^7.0.0",
+        "@babel/plugin-syntax-object-rest-spread": "^7.0.0",
+        "@babel/plugin-syntax-optional-catch-binding": "^7.0.0",
+        "@babel/plugin-transform-arrow-functions": "^7.0.0",
+        "@babel/plugin-transform-async-to-generator": "^7.0.0",
+        "@babel/plugin-transform-block-scoped-functions": "^7.0.0",
+        "@babel/plugin-transform-block-scoping": "^7.0.0",
+        "@babel/plugin-transform-classes": "^7.0.0",
+        "@babel/plugin-transform-computed-properties": "^7.0.0",
+        "@babel/plugin-transform-destructuring": "^7.0.0",
+        "@babel/plugin-transform-dotall-regex": "^7.0.0",
+        "@babel/plugin-transform-duplicate-keys": "^7.0.0",
+        "@babel/plugin-transform-exponentiation-operator": "^7.0.0",
+        "@babel/plugin-transform-for-of": "^7.0.0",
+        "@babel/plugin-transform-function-name": "^7.0.0",
+        "@babel/plugin-transform-literals": "^7.0.0",
+        "@babel/plugin-transform-modules-amd": "^7.0.0",
+        "@babel/plugin-transform-modules-commonjs": "^7.0.0",
+        "@babel/plugin-transform-modules-systemjs": "^7.0.0",
+        "@babel/plugin-transform-modules-umd": "^7.0.0",
+        "@babel/plugin-transform-new-target": "^7.0.0",
+        "@babel/plugin-transform-object-super": "^7.0.0",
+        "@babel/plugin-transform-parameters": "^7.0.0",
+        "@babel/plugin-transform-regenerator": "^7.0.0",
+        "@babel/plugin-transform-shorthand-properties": "^7.0.0",
+        "@babel/plugin-transform-spread": "^7.0.0",
+        "@babel/plugin-transform-sticky-regex": "^7.0.0",
+        "@babel/plugin-transform-template-literals": "^7.0.0",
+        "@babel/plugin-transform-typeof-symbol": "^7.0.0",
+        "@babel/plugin-transform-unicode-regex": "^7.0.0",
+        "browserslist": "^4.1.0",
+        "invariant": "^2.2.2",
+        "js-levenshtein": "^1.1.3",
+        "semver": "^5.3.0"
+      }
+    },
+    "@babel/preset-typescript": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.1.0.tgz",
+      "integrity": "sha512-LYveByuF9AOM8WrsNne5+N79k1YxjNB6gmpCQsnuSBAcV8QUeB+ZUxQzL7Rz7HksPbahymKkq2qBR+o36ggFZA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/plugin-transform-typescript": "^7.1.0"
+      }
+    },
+    "@babel/template": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz",
+      "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==",
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "@babel/parser": "^7.2.2",
+        "@babel/types": "^7.2.2"
+      }
+    },
+    "@babel/traverse": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.2.3.tgz",
+      "integrity": "sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==",
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "@babel/generator": "^7.2.2",
+        "@babel/helper-function-name": "^7.1.0",
+        "@babel/helper-split-export-declaration": "^7.0.0",
+        "@babel/parser": "^7.2.3",
+        "@babel/types": "^7.2.2",
+        "debug": "^4.1.0",
+        "globals": "^11.1.0",
+        "lodash": "^4.17.10"
+      }
+    },
+    "@babel/types": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.2.2.tgz",
+      "integrity": "sha512-fKCuD6UFUMkR541eDWL+2ih/xFZBXPOg/7EQFeTluMDebfqR4jrpaCjLhkWlQS4hT6nRa2PMEgXKbRB5/H2fpg==",
+      "requires": {
+        "esutils": "^2.0.2",
+        "lodash": "^4.17.10",
+        "to-fast-properties": "^2.0.0"
+      }
+    },
+    "@mrmlnc/readdir-enhanced": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
+      "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==",
+      "dev": true,
+      "requires": {
+        "call-me-maybe": "^1.0.1",
+        "glob-to-regexp": "^0.3.0"
+      }
+    },
+    "@nodelib/fs.stat": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
+      "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
+      "dev": true
+    },
+    "@posthtml/esm": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@posthtml/esm/-/esm-1.0.0.tgz",
+      "integrity": "sha512-dEVG+ITnvqKGa4v040tP+n8LOKOqr94qjLva7bE5pnfm2KHJwsKz69J4KMxgWLznbpBJzy8vQfCayEk3vLZnZQ==",
+      "dev": true
+    },
+    "@types/angular": {
+      "version": "1.6.51",
+      "resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.6.51.tgz",
+      "integrity": "sha512-wYU+/zlJWih7ZmonWVjGQ18tG7GboI9asMNjRBM5fpIFJWXSioQttCTw9qGL44cP82ghM8sCV9apEqm1zBDq2w==",
+      "dev": true
+    },
+    "@types/angular-animate": {
+      "version": "1.5.10",
+      "resolved": "https://registry.npmjs.org/@types/angular-animate/-/angular-animate-1.5.10.tgz",
+      "integrity": "sha512-MnYYvTHAPUbtT6gqwrnl6k3a03A5BlNz1nVlwVGxyS+MeWCX4DC14SJ/pgJUa8wj+J04wZ2prMxFsOfp6cyjjQ==",
+      "dev": true,
+      "requires": {
+        "@types/angular": "*"
+      }
+    },
+    "@types/angular-mocks": {
+      "version": "1.5.12",
+      "resolved": "https://registry.npmjs.org/@types/angular-mocks/-/angular-mocks-1.5.12.tgz",
+      "integrity": "sha512-nHar7zP5J6yK3UuqAR6RnV95jadt0X2tMxKiUpXF7w1VbY9YEfwYSHxMW/MCtbEs8NojKYphXZDuwV9dnCRNdw==",
+      "dev": true,
+      "requires": {
+        "@types/angular": "*"
+      }
+    },
+    "@types/angular-strap": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/@types/angular-strap/-/angular-strap-2.3.1.tgz",
+      "integrity": "sha512-PIIQbwgbxHRHeZ5sfGwjKG9PL2P6zh7KVt+V4lkHlNZSQctxRmu2fd0wfAurnwTnC0l+6SdgL+KEIghe7GOjYw==",
+      "dev": true,
+      "requires": {
+        "@types/angular": "*"
+      }
+    },
+    "@types/angular-translate": {
+      "version": "2.16.0",
+      "resolved": "https://registry.npmjs.org/@types/angular-translate/-/angular-translate-2.16.0.tgz",
+      "integrity": "sha512-ZW7w6+N95EVJSmi+PhIB4gGtl4ABpBOg6wFqk24+vXSIpExqZ88PnfX0O6jQwLDuKUh75rmQZRBy3KvF/JPx3Q==",
+      "dev": true,
+      "requires": {
+        "@types/angular": "*"
+      }
+    },
+    "@types/babel-types": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.4.tgz",
+      "integrity": "sha512-WiZhq3SVJHFRgRYLXvpf65XnV6ipVHhnNaNvE8yCimejrGglkg38kEj0JcizqwSHxmPSjcTlig/6JouxLGEhGw==",
+      "dev": true
+    },
+    "@types/babylon": {
+      "version": "6.16.4",
+      "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.4.tgz",
+      "integrity": "sha512-8dZMcGPno3g7pJ/d0AyJERo+lXh9i1JhDuCUs+4lNIN9eUe5Yh6UCLrpgSEi05Ve2JMLauL2aozdvKwNL0px1Q==",
+      "dev": true,
+      "requires": {
+        "@types/babel-types": "*"
+      }
+    },
+    "@types/bluebird": {
+      "version": "3.5.25",
+      "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.25.tgz",
+      "integrity": "sha512-yfhIBix+AIFTmYGtkC0Bi+XGjSkOINykqKvO/Wqdz/DuXlAKK7HmhLAXdPIGsV4xzKcL3ev/zYc4yLNo+OvGaw==",
+      "dev": true
+    },
+    "@types/chai": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.4.tgz",
+      "integrity": "sha512-h6+VEw2Vr3ORiFCyyJmcho2zALnUq9cvdB/IO8Xs9itrJVCenC7o26A6+m7D0ihTTr65eS259H5/Ghl/VjYs6g==",
+      "dev": true
+    },
+    "@types/copy-webpack-plugin": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/@types/copy-webpack-plugin/-/copy-webpack-plugin-4.4.2.tgz",
+      "integrity": "sha512-/L0m5kc7pKGpsu97TTgAP6YcVRmau2Wj0HpRPQBGEbZXT1DZkdozZPCZHGDWXpxcvWDFTxob2JmYJj3RC7CwFA==",
+      "dev": true,
+      "requires": {
+        "@types/minimatch": "*",
+        "@types/webpack": "*"
+      }
+    },
+    "@types/jquery": {
+      "version": "3.3.29",
+      "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.29.tgz",
+      "integrity": "sha512-FhJvBninYD36v3k6c+bVk1DSZwh7B5Dpb/Pyk3HKVsiohn0nhbefZZ+3JXbWQhFyt0MxSl2jRDdGQPHeOHFXrQ==",
+      "dev": true,
+      "requires": {
+        "@types/sizzle": "*"
+      }
+    },
+    "@types/karma": {
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/@types/karma/-/karma-1.7.4.tgz",
+      "integrity": "sha512-nKiGRJv4J3QKPIvUTOBFMkmNG0OAhCsrgoKZTm2+GY4YZ8TXL2CmSFJzeIzf7rvj/vtAitwj8HGcttB/yAAZDw==",
+      "dev": true,
+      "requires": {
+        "@types/bluebird": "*",
+        "@types/node": "*"
+      }
+    },
+    "@types/lodash": {
+      "version": "4.14.110",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.110.tgz",
+      "integrity": "sha512-iXYLa6olt4tnsCA+ZXeP6eEW3tk1SulWeYyP/yooWfAtXjozqXgtX4+XUtMuOCfYjKGz3F34++qUc3Q+TJuIIw==",
+      "dev": true
+    },
+    "@types/mini-css-extract-plugin": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/@types/mini-css-extract-plugin/-/mini-css-extract-plugin-0.2.0.tgz",
+      "integrity": "sha512-oHec+Vasp+K3C1Hb9HpwbA9Iw8ywqDgo9edWQJdBqxu05JH2AQsR56Zo5THpYbu1ieh/xJCvMRIHRdvrUBDmcA==",
+      "dev": true,
+      "requires": {
+        "@types/webpack": "*"
+      }
+    },
+    "@types/minimatch": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
+      "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
+      "dev": true
+    },
+    "@types/mocha": {
+      "version": "2.2.48",
+      "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.48.tgz",
+      "integrity": "sha512-nlK/iyETgafGli8Zh9zJVCTicvU3iajSkRwOh3Hhiva598CMqNJ4NcVCGMTGKpGpTYj/9R8RLzS9NAykSSCqGw==",
+      "dev": true
+    },
+    "@types/node": {
+      "version": "10.5.1",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.5.1.tgz",
+      "integrity": "sha512-AFLl1IALIuyt6oK4AYZsgWVJ/5rnyzQWud7IebaZWWV3YmgtPZkQmYio9R5Ze/2pdd7XfqF5bP+hWS11mAKoOQ==",
+      "dev": true
+    },
+    "@types/q": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.1.tgz",
+      "integrity": "sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA==",
+      "dev": true
+    },
+    "@types/sinon": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.0.0.tgz",
+      "integrity": "sha512-cuK4xM8Lg2wd8cxshcQa8RG4IK/xfyB6TNE6tNVvkrShR4xdrYgsV04q6Dp6v1Lp6biEFdzD8k8zg/ujQeiw+A==",
+      "dev": true
+    },
+    "@types/sizzle": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
+      "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
+      "dev": true
+    },
+    "@types/socket.io-client": {
+      "version": "1.4.32",
+      "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.32.tgz",
+      "integrity": "sha512-Vs55Kq8F+OWvy1RLA31rT+cAyemzgm0EWNeax6BWF8H7QiiOYMJIdcwSDdm5LVgfEkoepsWkS+40+WNb7BUMbg==",
+      "dev": true
+    },
+    "@types/tapable": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.4.tgz",
+      "integrity": "sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==",
+      "dev": true
+    },
+    "@types/uglify-js": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz",
+      "integrity": "sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ==",
+      "dev": true,
+      "requires": {
+        "source-map": "^0.6.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "@types/ui-grid": {
+      "version": "0.0.38",
+      "resolved": "https://registry.npmjs.org/@types/ui-grid/-/ui-grid-0.0.38.tgz",
+      "integrity": "sha512-xOe0ySy+PlaBf1lD9VQY9KRT3zpegDb80ivTj9lzwaQA4/mnA4tk9aFJQu4eKdKlivczA91WzFR53SyPCyTQvg==",
+      "dev": true,
+      "requires": {
+        "@types/angular": "*",
+        "@types/jquery": "*"
+      }
+    },
+    "@types/webpack": {
+      "version": "4.4.11",
+      "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.4.11.tgz",
+      "integrity": "sha512-NdESmbpvVEtJgs15kyZYKr5ouLYPMYt9DNG5JEgCekbG/ezFLPCzf4XcAv8caOb+b7x6ieAuSt0eoR0UkSI7RA==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*",
+        "@types/tapable": "*",
+        "@types/uglify-js": "*",
+        "source-map": "^0.6.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "@types/webpack-merge": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/@types/webpack-merge/-/webpack-merge-4.1.3.tgz",
+      "integrity": "sha512-VdmNuYIvIouYlCI73NLKOE1pOVAxv5m5eupvTemojZz9dqghoQXmeEveI6CqeuWpCH6x6FLp6+tXM2sls20/MA==",
+      "dev": true,
+      "requires": {
+        "@types/webpack": "*"
+      }
+    },
+    "@typescript-eslint/eslint-plugin": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.1.0.tgz",
+      "integrity": "sha512-uKP19jUxIIY8hfv99FKsKs1mEea+vOE9i4SxttGmDI1EMf7hpb5Who3NGYrXDlvZo/yFgoD1XeTk1b4H+Ako6g==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/parser": "1.1.0",
+        "requireindex": "^1.2.0"
+      }
+    },
+    "@typescript-eslint/parser": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.1.0.tgz",
+      "integrity": "sha512-Cs8ZPIcJBFmwSTXS+Qk7wiOC2i2jmj8Nuutk3/l7kqzYeQwDKYo4n0XHpodvUOqFnEcQt+SVfdUg90BfjuQdbg==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/typescript-estree": "1.1.0",
+        "eslint-scope": "^4.0.0",
+        "eslint-visitor-keys": "^1.0.0"
+      }
+    },
+    "@typescript-eslint/typescript-estree": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.1.0.tgz",
+      "integrity": "sha512-eABnKqJVv0Mm5uYon8Xw61SXldvOhWKDQdoZqsJ/YqEa9XvWV1URXdRvTOW8GLsKo4X3Un7pHKqKZhfbbUEGww==",
+      "dev": true,
+      "requires": {
+        "lodash.unescape": "4.0.1",
+        "semver": "5.5.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
+          "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
+          "dev": true
+        }
+      }
+    },
+    "@uirouter/angularjs": {
+      "version": "1.0.20",
+      "resolved": "https://registry.npmjs.org/@uirouter/angularjs/-/angularjs-1.0.20.tgz",
+      "integrity": "sha512-fY6bsesTL/tg8gyFHXaIjD1r5b7Ac+SYupodO9OzT4/gKI0YC+uGzLpQESAiXlT3fsxdEPVBzdtAbzXDwCKdEA==",
+      "requires": {
+        "@uirouter/core": "5.0.21"
+      },
+      "dependencies": {
+        "@uirouter/core": {
+          "version": "5.0.21",
+          "resolved": "https://registry.npmjs.org/@uirouter/core/-/core-5.0.21.tgz",
+          "integrity": "sha512-QWHc0wT00qtYNkT0BXZaFNLLHZyux0qJjF8c2WklW5/Q0Z866NoJjJErEMWjQb/AR+olkR2NlfEEi8KTQU2OpA=="
+        }
+      }
+    },
+    "@uirouter/core": {
+      "version": "5.0.19",
+      "resolved": "https://registry.npmjs.org/@uirouter/core/-/core-5.0.19.tgz",
+      "integrity": "sha512-wow+CKRThUAQkiTLNQCBsKQIU3NbH8GGH/w/TrcjKdvkZQA2jQB9QSqmmZxj7XNoZXY7QVcSSc4DWmxuSeAWmQ=="
+    },
+    "@uirouter/rx": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/@uirouter/rx/-/rx-0.5.0.tgz",
+      "integrity": "sha512-SHX1b8u8MW/7L6vFrKSRc5g5u/RgLakGMfdTeVQMSC84fc2yW7VDWNuGJhA8jiOTNKvzscuwvC02dxOgKcwrjg=="
+    },
+    "@webassemblyjs/ast": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.6.tgz",
+      "integrity": "sha512-8nkZS48EVsMUU0v6F1LCIOw4RYWLm2plMtbhFTjNgeXmsTNLuU3xTRtnljt9BFQB+iPbLRobkNrCWftWnNC7wQ==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/helper-module-context": "1.7.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.7.6",
+        "@webassemblyjs/wast-parser": "1.7.6",
+        "mamacro": "^0.0.3"
+      }
+    },
+    "@webassemblyjs/floating-point-hex-parser": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.6.tgz",
+      "integrity": "sha512-VBOZvaOyBSkPZdIt5VBMg3vPWxouuM13dPXGWI1cBh3oFLNcFJ8s9YA7S9l4mPI7+Q950QqOmqj06oa83hNWBA==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-api-error": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.6.tgz",
+      "integrity": "sha512-SCzhcQWHXfrfMSKcj8zHg1/kL9kb3aa5TN4plc/EREOs5Xop0ci5bdVBApbk2yfVi8aL+Ly4Qpp3/TRAUInjrg==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-buffer": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.6.tgz",
+      "integrity": "sha512-1/gW5NaGsEOZ02fjnFiU8/OEEXU1uVbv2um0pQ9YVL3IHSkyk6xOwokzyqqO1qDZQUAllb+V8irtClPWntbVqw==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-code-frame": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.6.tgz",
+      "integrity": "sha512-+suMJOkSn9+vEvDvgyWyrJo5vJsWSDXZmJAjtoUq4zS4eqHyXImpktvHOZwXp1XQjO5H+YQwsBgqTQEc0J/5zg==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/wast-printer": "1.7.6"
+      }
+    },
+    "@webassemblyjs/helper-fsm": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.6.tgz",
+      "integrity": "sha512-HCS6KN3wgxUihGBW7WFzEC/o8Eyvk0d56uazusnxXthDPnkWiMv+kGi9xXswL2cvfYfeK5yiM17z2K5BVlwypw==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-module-context": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.6.tgz",
+      "integrity": "sha512-e8/6GbY7OjLM+6OsN7f2krC2qYVNaSr0B0oe4lWdmq5sL++8dYDD1TFbD1TdAdWMRTYNr/Qq7ovXWzia2EbSjw==",
+      "dev": true,
+      "requires": {
+        "mamacro": "^0.0.3"
+      }
+    },
+    "@webassemblyjs/helper-wasm-bytecode": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.6.tgz",
+      "integrity": "sha512-PzYFCb7RjjSdAOljyvLWVqd6adAOabJW+8yRT+NWhXuf1nNZWH+igFZCUK9k7Cx7CsBbzIfXjJc7u56zZgFj9Q==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-wasm-section": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.6.tgz",
+      "integrity": "sha512-3GS628ppDPSuwcYlQ7cDCGr4W2n9c4hLzvnRKeuz+lGsJSmc/ADVoYpm1ts2vlB1tGHkjtQMni+yu8mHoMlKlA==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.7.6",
+        "@webassemblyjs/helper-buffer": "1.7.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.7.6",
+        "@webassemblyjs/wasm-gen": "1.7.6"
+      }
+    },
+    "@webassemblyjs/ieee754": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.7.6.tgz",
+      "integrity": "sha512-V4cIp0ruyw+hawUHwQLn6o2mFEw4t50tk530oKsYXQhEzKR+xNGDxs/SFFuyTO7X3NzEu4usA3w5jzhl2RYyzQ==",
+      "dev": true,
+      "requires": {
+        "@xtuc/ieee754": "^1.2.0"
+      }
+    },
+    "@webassemblyjs/leb128": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.7.6.tgz",
+      "integrity": "sha512-ojdlG8WpM394lBow4ncTGJoIVZ4aAtNOWHhfAM7m7zprmkVcKK+2kK5YJ9Bmj6/ketTtOn7wGSHCtMt+LzqgYQ==",
+      "dev": true,
+      "requires": {
+        "@xtuc/long": "4.2.1"
+      }
+    },
+    "@webassemblyjs/utf8": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.7.6.tgz",
+      "integrity": "sha512-oId+tLxQ+AeDC34ELRYNSqJRaScB0TClUU6KQfpB8rNT6oelYlz8axsPhf6yPTg7PBJ/Z5WcXmUYiHEWgbbHJw==",
+      "dev": true
+    },
+    "@webassemblyjs/wasm-edit": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.6.tgz",
+      "integrity": "sha512-pTNjLO3o41v/Vz9VFLl+I3YLImpCSpodFW77pNoH4agn5I6GgSxXHXtvWDTvYJFty0jSeXZWLEmbaSIRUDlekg==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.7.6",
+        "@webassemblyjs/helper-buffer": "1.7.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.7.6",
+        "@webassemblyjs/helper-wasm-section": "1.7.6",
+        "@webassemblyjs/wasm-gen": "1.7.6",
+        "@webassemblyjs/wasm-opt": "1.7.6",
+        "@webassemblyjs/wasm-parser": "1.7.6",
+        "@webassemblyjs/wast-printer": "1.7.6"
+      }
+    },
+    "@webassemblyjs/wasm-gen": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.6.tgz",
+      "integrity": "sha512-mQvFJVumtmRKEUXMohwn8nSrtjJJl6oXwF3FotC5t6e2hlKMh8sIaW03Sck2MDzw9xPogZD7tdP5kjPlbH9EcQ==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.7.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.7.6",
+        "@webassemblyjs/ieee754": "1.7.6",
+        "@webassemblyjs/leb128": "1.7.6",
+        "@webassemblyjs/utf8": "1.7.6"
+      }
+    },
+    "@webassemblyjs/wasm-opt": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.6.tgz",
+      "integrity": "sha512-go44K90fSIsDwRgtHhX14VtbdDPdK2sZQtZqUcMRvTojdozj5tLI0VVJAzLCfz51NOkFXezPeVTAYFqrZ6rI8Q==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.7.6",
+        "@webassemblyjs/helper-buffer": "1.7.6",
+        "@webassemblyjs/wasm-gen": "1.7.6",
+        "@webassemblyjs/wasm-parser": "1.7.6"
+      }
+    },
+    "@webassemblyjs/wasm-parser": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.6.tgz",
+      "integrity": "sha512-t1T6TfwNY85pDA/HWPA8kB9xA4sp9ajlRg5W7EKikqrynTyFo+/qDzIpvdkOkOGjlS6d4n4SX59SPuIayR22Yg==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.7.6",
+        "@webassemblyjs/helper-api-error": "1.7.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.7.6",
+        "@webassemblyjs/ieee754": "1.7.6",
+        "@webassemblyjs/leb128": "1.7.6",
+        "@webassemblyjs/utf8": "1.7.6"
+      }
+    },
+    "@webassemblyjs/wast-parser": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.7.6.tgz",
+      "integrity": "sha512-1MaWTErN0ziOsNUlLdvwS+NS1QWuI/kgJaAGAMHX8+fMJFgOJDmN/xsG4h/A1Gtf/tz5VyXQciaqHZqp2q0vfg==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.7.6",
+        "@webassemblyjs/floating-point-hex-parser": "1.7.6",
+        "@webassemblyjs/helper-api-error": "1.7.6",
+        "@webassemblyjs/helper-code-frame": "1.7.6",
+        "@webassemblyjs/helper-fsm": "1.7.6",
+        "@xtuc/long": "4.2.1",
+        "mamacro": "^0.0.3"
+      }
+    },
+    "@webassemblyjs/wast-printer": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.7.6.tgz",
+      "integrity": "sha512-vHdHSK1tOetvDcl1IV1OdDeGNe/NDDQ+KzuZHMtqTVP1xO/tZ/IKNpj5BaGk1OYFdsDWQqb31PIwdEyPntOWRQ==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.7.6",
+        "@webassemblyjs/wast-parser": "1.7.6",
+        "@xtuc/long": "4.2.1"
+      }
+    },
+    "@xtuc/ieee754": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+      "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+      "dev": true
+    },
+    "@xtuc/long": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.1.tgz",
+      "integrity": "sha512-FZdkNBDqBRHKQ2MEbSC17xnPFOhZxeJ2YGSfr2BKf3sujG49Qe3bB+rGCwQfIaA7WHnGeGkSijX4FuBCdrzW/g==",
+      "dev": true
+    },
+    "abbrev": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+      "dev": true
+    },
+    "accepts": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
+      "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
+      "dev": true,
+      "requires": {
+        "mime-types": "~2.1.18",
+        "negotiator": "0.6.1"
+      }
+    },
+    "acorn": {
+      "version": "5.7.3",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
+      "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==",
+      "dev": true
+    },
+    "acorn-globals": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz",
+      "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=",
+      "dev": true,
+      "requires": {
+        "acorn": "^4.0.4"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "4.0.13",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
+          "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=",
+          "dev": true
+        }
+      }
+    },
+    "acorn-jsx": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz",
+      "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==",
+      "dev": true
+    },
+    "adjust-sourcemap-loader": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-1.2.0.tgz",
+      "integrity": "sha512-958oaHHVEXMvsY7v7cC5gEkNIcoaAVIhZ4mBReYVZJOTP9IgKmzLjIOhTtzpLMu+qriXvLsVjJ155EeInp45IQ==",
+      "dev": true,
+      "requires": {
+        "assert": "^1.3.0",
+        "camelcase": "^1.2.1",
+        "loader-utils": "^1.1.0",
+        "lodash.assign": "^4.0.1",
+        "lodash.defaults": "^3.1.2",
+        "object-path": "^0.9.2",
+        "regex-parser": "^2.2.9"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
+          "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
+          "dev": true
+        },
+        "lodash.defaults": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-3.1.2.tgz",
+          "integrity": "sha1-xzCLGNv4vJNy1wGnNJPGEZK9Liw=",
+          "dev": true,
+          "requires": {
+            "lodash.assign": "^3.0.0",
+            "lodash.restparam": "^3.0.0"
+          },
+          "dependencies": {
+            "lodash.assign": {
+              "version": "3.2.0",
+              "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz",
+              "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=",
+              "dev": true,
+              "requires": {
+                "lodash._baseassign": "^3.0.0",
+                "lodash._createassigner": "^3.0.0",
+                "lodash.keys": "^3.0.0"
+              }
+            }
+          }
+        }
+      }
+    },
+    "after": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+      "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
+    },
+    "ajv": {
+      "version": "5.5.2",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+      "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+      "dev": true,
+      "requires": {
+        "co": "^4.6.0",
+        "fast-deep-equal": "^1.0.0",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.3.0"
+      }
+    },
+    "ajv-errors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
+      "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
+      "dev": true
+    },
+    "ajv-keywords": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz",
+      "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=",
+      "dev": true
+    },
+    "align-text": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
+      "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2",
+        "longest": "^1.0.1",
+        "repeat-string": "^1.5.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "alphanum-sort": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz",
+      "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
+      "dev": true
+    },
+    "amdefine": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+      "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
+      "dev": true
+    },
+    "angular": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/angular/-/angular-1.7.6.tgz",
+      "integrity": "sha512-QELpvuMIe1FTGniAkRz93O6A+di0yu88niDwcdzrSqtUHNtZMgtgFS4f7W/6Gugbuwej8Kyswlmymwdp8iPCWg=="
+    },
+    "angular-acl": {
+      "version": "0.1.10",
+      "resolved": "https://registry.npmjs.org/angular-acl/-/angular-acl-0.1.10.tgz",
+      "integrity": "sha1-4UI97jgmLXowieJm9tk9qxBv5Os="
+    },
+    "angular-animate": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.7.6.tgz",
+      "integrity": "sha512-WPzJkmpVQut8SrTPEjAImRqPfcB2E6TsZDndF7Z7Yu5hBDUIvOM/m62rWxLiWhQg2DlbDuejz6mKQwTakgMThA=="
+    },
+    "angular-aria": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/angular-aria/-/angular-aria-1.7.6.tgz",
+      "integrity": "sha512-Odq3piJl1uc85h0vpHO+0ZSfqY+nc4PNmjl+9dIEkTBpfH+dGvGQxaldpTL54YQIP09hOPi7jkrRlEYa/rMzsg=="
+    },
+    "angular-drag-and-drop-lists": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/angular-drag-and-drop-lists/-/angular-drag-and-drop-lists-1.4.0.tgz",
+      "integrity": "sha1-KREEyTXhCkL4RZaW7TAGlQnXm/w="
+    },
+    "angular-gridster": {
+      "version": "0.13.14",
+      "resolved": "https://registry.npmjs.org/angular-gridster/-/angular-gridster-0.13.14.tgz",
+      "integrity": "sha1-er6/Y9fJ++xFOLnMpFfg2oBdQgQ="
+    },
+    "angular-messages": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/angular-messages/-/angular-messages-1.7.6.tgz",
+      "integrity": "sha512-ch+J1SjK3gbfc7N6epG2+nov59Fdc+8i6Hl+DYxwtlL1X4D3ZhYGymL65gG6fc+yyqBWhZ8athr+lgd5qcu77Q=="
+    },
+    "angular-mocks": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.7.6.tgz",
+      "integrity": "sha512-t3eQmuAZczdOVdOQj7muCBwH+MBNwd+/FaAsV1SNp+597EQVWABQwxI6KXE0k0ZlyJ5JbtkNIKU8kGAj1znxhw==",
+      "dev": true
+    },
+    "angular-nvd3": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/angular-nvd3/-/angular-nvd3-1.0.9.tgz",
+      "integrity": "sha1-jYpxSH2eWhIuil0PXNJqsOSRpvU=",
+      "requires": {
+        "angular": "^1.x",
+        "d3": "^3.3",
+        "nvd3": "^1.7.1"
+      }
+    },
+    "angular-sanitize": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.7.6.tgz",
+      "integrity": "sha512-wDRCQ6VAHzHmEWgpC9nb1ea74IGJgP3dCUgF9DHtHQJSeNvAB/2e4glxfogS3tzGOEnppzLEOD1v+m5lJeEo1Q=="
+    },
+    "angular-smart-table": {
+      "version": "2.1.11",
+      "resolved": "https://registry.npmjs.org/angular-smart-table/-/angular-smart-table-2.1.11.tgz",
+      "integrity": "sha512-8ZSGygQqVBkpPDzb0382ujK1eOr8fnePXpzq+sUBkUsvRaEgMeY6VoFpWmg106472mD6oMNXIW9+VDQKllXa0w=="
+    },
+    "angular-strap": {
+      "version": "2.3.12",
+      "resolved": "https://registry.npmjs.org/angular-strap/-/angular-strap-2.3.12.tgz",
+      "integrity": "sha1-+uIWVc13B5Zxv5GKt+1iJ3gwYvo="
+    },
+    "angular-translate": {
+      "version": "2.18.1",
+      "resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.18.1.tgz",
+      "integrity": "sha512-Mw0kFBqsv5j8ItL9IhRZunIlVmIRW6iFsiTmRs9wGr2QTt8z4rehYlWyHos8qnXc/kyOYJiW50iH50CSNHGB9A==",
+      "requires": {
+        "angular": ">=1.2.26 <=1.7"
+      }
+    },
+    "angular-tree-control": {
+      "version": "0.2.28",
+      "resolved": "https://registry.npmjs.org/angular-tree-control/-/angular-tree-control-0.2.28.tgz",
+      "integrity": "sha1-bPWNWQ7o4FA7uoma5SmuS4dBCDs="
+    },
+    "angular-ui-grid": {
+      "version": "4.6.1",
+      "resolved": "https://registry.npmjs.org/angular-ui-grid/-/angular-ui-grid-4.6.1.tgz",
+      "integrity": "sha512-IQqJRme54LSBRa7AJygHuiex2aGBdhSTU6COoRSFTkBJyAmQp1oLyy9a68WxBFG0nEA3eoi36On5gkM4Ssy0Iw==",
+      "requires": {
+        "angular": ">=1.4.0 1.7.x"
+      }
+    },
+    "angular-ui-validate": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/angular-ui-validate/-/angular-ui-validate-1.2.3.tgz",
+      "integrity": "sha1-vrFB9kQJv926ZcEvdYG4k931kyE="
+    },
+    "angular1-async-filter": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/angular1-async-filter/-/angular1-async-filter-1.1.0.tgz",
+      "integrity": "sha1-0CtjYqwbH5ZsXQ7Y8P0ri0DJP3g="
+    },
+    "ansi-colors": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
+      "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==",
+      "dev": true
+    },
+    "ansi-escapes": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
+      "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==",
+      "dev": true
+    },
+    "ansi-html": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz",
+      "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=",
+      "dev": true
+    },
+    "ansi-regex": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz",
+      "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk="
+    },
+    "ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "requires": {
+        "color-convert": "^1.9.0"
+      }
+    },
+    "anymatch": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+      "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+      "dev": true,
+      "requires": {
+        "micromatch": "^3.1.4",
+        "normalize-path": "^2.1.1"
+      }
+    },
+    "app-root-path": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.0.1.tgz",
+      "integrity": "sha1-zWLc+OT9WkF+/GZNLlsQZTxlG0Y=",
+      "dev": true
+    },
+    "aproba": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+      "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+      "dev": true
+    },
+    "are-we-there-yet": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
+      "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+      "dev": true,
+      "requires": {
+        "delegates": "^1.0.0",
+        "readable-stream": "^2.0.6"
+      }
+    },
+    "argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "dev": true,
+      "requires": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "arr-diff": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+      "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+      "dev": true
+    },
+    "arr-flatten": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+      "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+      "dev": true
+    },
+    "arr-union": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+      "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
+      "dev": true
+    },
+    "array-find-index": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+      "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
+      "dev": true
+    },
+    "array-flatten": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
+      "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==",
+      "dev": true
+    },
+    "array-slice": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
+      "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=",
+      "dev": true
+    },
+    "array-union": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+      "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+      "dev": true,
+      "requires": {
+        "array-uniq": "^1.0.1"
+      }
+    },
+    "array-uniq": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+      "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
+      "dev": true
+    },
+    "array-unique": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+      "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+      "dev": true
+    },
+    "arraybuffer.slice": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
+      "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
+    },
+    "asap": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+      "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
+      "dev": true
+    },
+    "asn1": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+      "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+      "dev": true,
+      "requires": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
+    "asn1.js": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
+      "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
+    "assert": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
+      "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
+      "dev": true,
+      "requires": {
+        "util": "0.10.3"
+      },
+      "dependencies": {
+        "inherits": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
+          "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
+          "dev": true
+        },
+        "util": {
+          "version": "0.10.3",
+          "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+          "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
+          "dev": true,
+          "requires": {
+            "inherits": "2.0.1"
+          }
+        }
+      }
+    },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+      "dev": true
+    },
+    "assertion-error": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+      "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+      "dev": true
+    },
+    "assign-symbols": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+      "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
+      "dev": true
+    },
+    "astral-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
+      "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
+      "dev": true
+    },
+    "async": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
+      "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.17.10"
+      }
+    },
+    "async-each": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
+      "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
+      "dev": true
+    },
+    "async-foreach": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz",
+      "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=",
+      "dev": true
+    },
+    "async-limiter": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
+      "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+      "dev": true
+    },
+    "atob": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+      "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+      "dev": true
+    },
+    "autoprefixer": {
+      "version": "6.7.7",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz",
+      "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=",
+      "dev": true,
+      "requires": {
+        "browserslist": "^1.7.6",
+        "caniuse-db": "^1.0.30000634",
+        "normalize-range": "^0.1.2",
+        "num2fraction": "^1.2.2",
+        "postcss": "^5.2.16",
+        "postcss-value-parser": "^3.2.3"
+      },
+      "dependencies": {
+        "browserslist": {
+          "version": "1.7.7",
+          "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz",
+          "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=",
+          "dev": true,
+          "requires": {
+            "caniuse-db": "^1.0.30000639",
+            "electron-to-chromium": "^1.2.7"
+          }
+        }
+      }
+    },
+    "aws-sign2": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+      "dev": true
+    },
+    "aws4": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+      "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
+      "dev": true
+    },
+    "babel-code-frame": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+      "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "esutils": "^2.0.2",
+        "js-tokens": "^3.0.2"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "has-ansi": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+          "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "js-tokens": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+          "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
+    "babel-loader": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.2.tgz",
+      "integrity": "sha512-Law0PGtRV1JL8Y9Wpzc0d6EE0GD7LzXWCfaeWwboUMcBWNG6gvaWTK1/+BK7a4X5EmeJiGEuDDFxUsOa8RSWCw==",
+      "dev": true,
+      "requires": {
+        "find-cache-dir": "^1.0.0",
+        "loader-utils": "^1.0.2",
+        "mkdirp": "^0.5.1",
+        "util.promisify": "^1.0.0"
+      }
+    },
+    "babel-runtime": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+      "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+      "dev": true,
+      "requires": {
+        "core-js": "^2.4.0",
+        "regenerator-runtime": "^0.11.0"
+      }
+    },
+    "babel-types": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz",
+      "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "esutils": "^2.0.2",
+        "lodash": "^4.17.4",
+        "to-fast-properties": "^1.0.3"
+      },
+      "dependencies": {
+        "to-fast-properties": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
+          "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=",
+          "dev": true
+        }
+      }
+    },
+    "babylon": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
+      "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
+      "dev": true
+    },
+    "backo2": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+      "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+      "dev": true
+    },
+    "base": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+      "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+      "dev": true,
+      "requires": {
+        "cache-base": "^1.0.1",
+        "class-utils": "^0.3.5",
+        "component-emitter": "^1.2.1",
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.1",
+        "mixin-deep": "^1.2.0",
+        "pascalcase": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "base64-arraybuffer": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+      "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
+    },
+    "base64-js": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
+      "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==",
+      "dev": true
+    },
+    "base64id": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+      "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=",
+      "dev": true
+    },
+    "batch": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
+      "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
+      "dev": true
+    },
+    "bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+      "dev": true,
+      "requires": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
+    "better-assert": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+      "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+      "requires": {
+        "callsite": "1.0.0"
+      }
+    },
+    "big.js": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
+      "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
+      "dev": true
+    },
+    "bignumber.js": {
+      "version": "7.2.1",
+      "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz",
+      "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ=="
+    },
+    "binary-extensions": {
+      "version": "1.12.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz",
+      "integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==",
+      "dev": true
+    },
+    "blob": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
+      "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
+    },
+    "block-stream": {
+      "version": "0.0.9",
+      "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
+      "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
+      "dev": true,
+      "requires": {
+        "inherits": "~2.0.0"
+      }
+    },
+    "bluebird": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
+      "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==",
+      "dev": true
+    },
+    "bn.js": {
+      "version": "4.11.8",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
+      "dev": true
+    },
+    "body-parser": {
+      "version": "1.18.3",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
+      "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
+      "dev": true,
+      "requires": {
+        "bytes": "3.0.0",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "http-errors": "~1.6.3",
+        "iconv-lite": "0.4.23",
+        "on-finished": "~2.3.0",
+        "qs": "6.5.2",
+        "raw-body": "2.3.3",
+        "type-is": "~1.6.16"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "iconv-lite": {
+          "version": "0.4.23",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
+          "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
+          "dev": true,
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "bonjour": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
+      "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=",
+      "dev": true,
+      "requires": {
+        "array-flatten": "^2.1.0",
+        "deep-equal": "^1.0.1",
+        "dns-equal": "^1.0.0",
+        "dns-txt": "^2.0.2",
+        "multicast-dns": "^6.0.1",
+        "multicast-dns-service-types": "^1.1.0"
+      }
+    },
+    "boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
+      "dev": true
+    },
+    "bootstrap-sass": {
+      "version": "3.3.7",
+      "resolved": "https://registry.npmjs.org/bootstrap-sass/-/bootstrap-sass-3.3.7.tgz",
+      "integrity": "sha1-ZZbHq0D2Y3OTMjqwvIDQZPxjBJg=",
+      "dev": true
+    },
+    "brace": {
+      "version": "0.11.1",
+      "resolved": "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz",
+      "integrity": "sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg="
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+      "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+      "dev": true,
+      "requires": {
+        "arr-flatten": "^1.1.0",
+        "array-unique": "^0.3.2",
+        "extend-shallow": "^2.0.1",
+        "fill-range": "^4.0.0",
+        "isobject": "^3.0.1",
+        "repeat-element": "^1.1.2",
+        "snapdragon": "^0.8.1",
+        "snapdragon-node": "^2.0.1",
+        "split-string": "^3.0.2",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
+      "dev": true
+    },
+    "browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+      "dev": true
+    },
+    "browser-update": {
+      "version": "3.1.13",
+      "resolved": "https://registry.npmjs.org/browser-update/-/browser-update-3.1.13.tgz",
+      "integrity": "sha1-kmT5iJFW4vu939T9l15p0Dg2nG0="
+    },
+    "browserify-aes": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+      "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+      "dev": true,
+      "requires": {
+        "buffer-xor": "^1.0.3",
+        "cipher-base": "^1.0.0",
+        "create-hash": "^1.1.0",
+        "evp_bytestokey": "^1.0.3",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "browserify-cipher": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+      "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
+      "dev": true,
+      "requires": {
+        "browserify-aes": "^1.0.4",
+        "browserify-des": "^1.0.0",
+        "evp_bytestokey": "^1.0.0"
+      }
+    },
+    "browserify-des": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
+      "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "^1.0.1",
+        "des.js": "^1.0.0",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.1.2"
+      }
+    },
+    "browserify-rsa": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+      "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "randombytes": "^2.0.1"
+      }
+    },
+    "browserify-sign": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
+      "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.1",
+        "browserify-rsa": "^4.0.0",
+        "create-hash": "^1.1.0",
+        "create-hmac": "^1.1.2",
+        "elliptic": "^6.0.0",
+        "inherits": "^2.0.1",
+        "parse-asn1": "^5.0.0"
+      }
+    },
+    "browserify-zlib": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+      "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+      "dev": true,
+      "requires": {
+        "pako": "~1.0.5"
+      }
+    },
+    "browserslist": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.3.7.tgz",
+      "integrity": "sha512-pWQv51Ynb0MNk9JGMCZ8VkM785/4MQNXiFYtPqI7EEP0TJO+/d/NqRVn1uiAN0DNbnlUSpL2sh16Kspasv3pUQ==",
+      "dev": true,
+      "requires": {
+        "caniuse-lite": "^1.0.30000925",
+        "electron-to-chromium": "^1.3.96",
+        "node-releases": "^1.1.3"
+      }
+    },
+    "bson-objectid": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/bson-objectid/-/bson-objectid-1.1.5.tgz",
+      "integrity": "sha1-S54hCpjBxOqp7fY6ygeEyzC3m14="
+    },
+    "buffer-alloc": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
+      "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
+      "dev": true,
+      "requires": {
+        "buffer-alloc-unsafe": "^1.1.0",
+        "buffer-fill": "^1.0.0"
+      }
+    },
+    "buffer-alloc-unsafe": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
+      "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
+      "dev": true
+    },
+    "buffer-fill": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
+      "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=",
+      "dev": true
+    },
+    "buffer-from": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+      "dev": true
+    },
+    "buffer-indexof": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz",
+      "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==",
+      "dev": true
+    },
+    "buffer-xor": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
+      "dev": true
+    },
+    "builtin-modules": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+      "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+      "dev": true
+    },
+    "builtin-status-codes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+      "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
+      "dev": true
+    },
+    "bytes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+      "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
+      "dev": true
+    },
+    "cacache": {
+      "version": "10.0.4",
+      "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz",
+      "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==",
+      "dev": true,
+      "requires": {
+        "bluebird": "^3.5.1",
+        "chownr": "^1.0.1",
+        "glob": "^7.1.2",
+        "graceful-fs": "^4.1.11",
+        "lru-cache": "^4.1.1",
+        "mississippi": "^2.0.0",
+        "mkdirp": "^0.5.1",
+        "move-concurrently": "^1.0.1",
+        "promise-inflight": "^1.0.1",
+        "rimraf": "^2.6.2",
+        "ssri": "^5.2.4",
+        "unique-filename": "^1.1.0",
+        "y18n": "^4.0.0"
+      }
+    },
+    "cache-base": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+      "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+      "dev": true,
+      "requires": {
+        "collection-visit": "^1.0.0",
+        "component-emitter": "^1.2.1",
+        "get-value": "^2.0.6",
+        "has-value": "^1.0.0",
+        "isobject": "^3.0.1",
+        "set-value": "^2.0.0",
+        "to-object-path": "^0.3.0",
+        "union-value": "^1.0.0",
+        "unset-value": "^1.0.0"
+      }
+    },
+    "call-me-maybe": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz",
+      "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=",
+      "dev": true
+    },
+    "callsite": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+      "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
+    },
+    "callsites": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.0.0.tgz",
+      "integrity": "sha512-tWnkwu9YEq2uzlBDI4RcLn8jrFvF9AOi8PxDNU3hZZjJcjkcRAq3vCI+vZcg1SuxISDYe86k9VZFwAxDiJGoAw==",
+      "dev": true
+    },
+    "camel-case": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
+      "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
+      "dev": true,
+      "requires": {
+        "no-case": "^2.2.0",
+        "upper-case": "^1.1.1"
+      }
+    },
+    "camelcase": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+      "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+      "dev": true
+    },
+    "camelcase-keys": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+      "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+      "dev": true,
+      "requires": {
+        "camelcase": "^2.0.0",
+        "map-obj": "^1.0.0"
+      }
+    },
+    "caniuse-api": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz",
+      "integrity": "sha1-tTTnxzTE+B7F++isoq0kNUuWLGw=",
+      "dev": true,
+      "requires": {
+        "browserslist": "^1.3.6",
+        "caniuse-db": "^1.0.30000529",
+        "lodash.memoize": "^4.1.2",
+        "lodash.uniq": "^4.5.0"
+      },
+      "dependencies": {
+        "browserslist": {
+          "version": "1.7.7",
+          "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz",
+          "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=",
+          "dev": true,
+          "requires": {
+            "caniuse-db": "^1.0.30000639",
+            "electron-to-chromium": "^1.2.7"
+          }
+        }
+      }
+    },
+    "caniuse-db": {
+      "version": "1.0.30000928",
+      "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000928.tgz",
+      "integrity": "sha512-nAoeTspAEzLjqGSeibzM09WojORi08faeOOI5GBmFWC3/brydovb9lYJWM+p48rEQsdevfpufK58gPiDtwOWKw==",
+      "dev": true
+    },
+    "caniuse-lite": {
+      "version": "1.0.30000928",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000928.tgz",
+      "integrity": "sha512-aSpMWRXL6ZXNnzm8hgE4QDLibG5pVJ2Ujzsuj3icazlIkxXkPXtL+BWnMx6FBkWmkZgBHGUxPZQvrbRw2ZTxhg==",
+      "dev": true
+    },
+    "caseless": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+      "dev": true
+    },
+    "center-align": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
+      "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=",
+      "dev": true,
+      "requires": {
+        "align-text": "^0.1.3",
+        "lazy-cache": "^1.0.3"
+      }
+    },
+    "chai": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.0.tgz",
+      "integrity": "sha1-MxoDkbVcOvh0CunDt0WLwcOAXm0=",
+      "dev": true,
+      "requires": {
+        "assertion-error": "^1.0.1",
+        "check-error": "^1.0.1",
+        "deep-eql": "^2.0.1",
+        "get-func-name": "^2.0.0",
+        "pathval": "^1.0.0",
+        "type-detect": "^4.0.0"
+      }
+    },
+    "chalk": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz",
+      "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==",
+      "requires": {
+        "ansi-styles": "^3.1.0",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^4.0.0"
+      },
+      "dependencies": {
+        "has-flag": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
+          "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE="
+        },
+        "supports-color": {
+          "version": "4.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz",
+          "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=",
+          "requires": {
+            "has-flag": "^2.0.0"
+          }
+        }
+      }
+    },
+    "character-parser": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
+      "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=",
+      "dev": true,
+      "requires": {
+        "is-regex": "^1.0.3"
+      }
+    },
+    "chardet": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+      "dev": true
+    },
+    "chart.js": {
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.7.2.tgz",
+      "integrity": "sha512-90wl3V9xRZ8tnMvMlpcW+0Yg13BelsGS9P9t0ClaDxv/hdypHDr/YAGf+728m11P5ljwyB0ZHfPKCapZFqSqYA==",
+      "requires": {
+        "chartjs-color": "^2.1.0",
+        "moment": "^2.10.2"
+      }
+    },
+    "chartjs-color": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.2.0.tgz",
+      "integrity": "sha1-hKL7dVeH7YXDndbdjHsdiEKbrq4=",
+      "requires": {
+        "chartjs-color-string": "^0.5.0",
+        "color-convert": "^0.5.3"
+      },
+      "dependencies": {
+        "color-convert": {
+          "version": "0.5.3",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz",
+          "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0="
+        }
+      }
+    },
+    "chartjs-color-string": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz",
+      "integrity": "sha512-amWNvCOXlOUYxZVDSa0YOab5K/lmEhbFNKI55PWc4mlv28BDzA7zaoQTGxSBgJMHIW+hGX8YUrvw/FH4LyhwSQ==",
+      "requires": {
+        "color-name": "^1.0.0"
+      }
+    },
+    "chartjs-plugin-streaming": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/chartjs-plugin-streaming/-/chartjs-plugin-streaming-1.6.1.tgz",
+      "integrity": "sha512-ZKTr50pU9LvG/Yf+wX0cUJAI8qlS7t8botuC3+5Jv4N16s5IuRY1GOyVbVzf80nEueOv1/zY2GAhaPk2M4PYbQ==",
+      "requires": {
+        "chart.js": "^2.7.0",
+        "moment": "^2.10.2"
+      }
+    },
+    "check-error": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+      "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
+      "dev": true
+    },
+    "chokidar": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.2.tgz",
+      "integrity": "sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==",
+      "dev": true,
+      "requires": {
+        "anymatch": "^2.0.0",
+        "async-each": "^1.0.1",
+        "braces": "^2.3.2",
+        "fsevents": "^1.2.7",
+        "glob-parent": "^3.1.0",
+        "inherits": "^2.0.3",
+        "is-binary-path": "^1.0.0",
+        "is-glob": "^4.0.0",
+        "normalize-path": "^3.0.0",
+        "path-is-absolute": "^1.0.0",
+        "readdirp": "^2.2.1",
+        "upath": "^1.1.0"
+      },
+      "dependencies": {
+        "normalize-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+          "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+          "dev": true
+        }
+      }
+    },
+    "chownr": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
+      "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
+      "dev": true
+    },
+    "chrome-trace-event": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz",
+      "integrity": "sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==",
+      "dev": true,
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "cipher-base": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+      "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "circular-json": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz",
+      "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==",
+      "dev": true
+    },
+    "clap": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz",
+      "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "has-ansi": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+          "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
+    "class-utils": {
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+      "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+      "dev": true,
+      "requires": {
+        "arr-union": "^3.1.0",
+        "define-property": "^0.2.5",
+        "isobject": "^3.0.0",
+        "static-extend": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
+    "clean-css": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
+      "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==",
+      "dev": true,
+      "requires": {
+        "source-map": "~0.6.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "cli-cursor": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+      "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+      "dev": true,
+      "requires": {
+        "restore-cursor": "^2.0.0"
+      }
+    },
+    "cli-width": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
+      "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
+      "dev": true
+    },
+    "cliui": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+      "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+      "dev": true,
+      "requires": {
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.1",
+        "wrap-ansi": "^2.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+          "dev": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "dev": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        }
+      }
+    },
+    "clone": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+      "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
+      "dev": true
+    },
+    "clone-deep": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz",
+      "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==",
+      "dev": true,
+      "requires": {
+        "for-own": "^1.0.0",
+        "is-plain-object": "^2.0.4",
+        "kind-of": "^6.0.0",
+        "shallow-clone": "^1.0.0"
+      },
+      "dependencies": {
+        "for-own": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+          "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
+          "dev": true,
+          "requires": {
+            "for-in": "^1.0.1"
+          }
+        }
+      }
+    },
+    "co": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+      "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
+      "dev": true
+    },
+    "coa": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz",
+      "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=",
+      "dev": true,
+      "requires": {
+        "q": "^1.1.2"
+      }
+    },
+    "code-point-at": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+      "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+      "dev": true
+    },
+    "collection-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+      "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+      "dev": true,
+      "requires": {
+        "map-visit": "^1.0.0",
+        "object-visit": "^1.0.0"
+      }
+    },
+    "color": {
+      "version": "0.11.4",
+      "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz",
+      "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=",
+      "dev": true,
+      "requires": {
+        "clone": "^1.0.2",
+        "color-convert": "^1.3.0",
+        "color-string": "^0.3.0"
+      }
+    },
+    "color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "requires": {
+        "color-name": "1.1.3"
+      }
+    },
+    "color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+    },
+    "color-string": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz",
+      "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=",
+      "dev": true,
+      "requires": {
+        "color-name": "^1.0.0"
+      }
+    },
+    "colormin": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/colormin/-/colormin-1.1.2.tgz",
+      "integrity": "sha1-6i90IKcrlogaOKrlnsEkpvcpgTM=",
+      "dev": true,
+      "requires": {
+        "color": "^0.11.0",
+        "css-color-names": "0.0.4",
+        "has": "^1.0.1"
+      }
+    },
+    "colors": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
+      "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
+      "dev": true
+    },
+    "combine-lists": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz",
+      "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.5.0"
+      }
+    },
+    "combined-stream": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
+      "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
+      "dev": true,
+      "requires": {
+        "delayed-stream": "~1.0.0"
+      }
+    },
+    "commander": {
+      "version": "2.17.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
+      "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
+      "dev": true
+    },
+    "commondir": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+      "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
+      "dev": true
+    },
+    "component-bind": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+      "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
+    },
+    "component-emitter": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+      "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
+    },
+    "component-inherit": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+      "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
+    },
+    "compressible": {
+      "version": "2.0.16",
+      "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.16.tgz",
+      "integrity": "sha512-JQfEOdnI7dASwCuSPWIeVYwc/zMsu/+tRhoUvEfXz2gxOA2DNjmG5vhtFdBlhWPPGo+RdT9S3tgc/uH5qgDiiA==",
+      "dev": true,
+      "requires": {
+        "mime-db": ">= 1.38.0 < 2"
+      },
+      "dependencies": {
+        "mime-db": {
+          "version": "1.38.0",
+          "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz",
+          "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==",
+          "dev": true
+        }
+      }
+    },
+    "compression": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz",
+      "integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==",
+      "dev": true,
+      "requires": {
+        "accepts": "~1.3.5",
+        "bytes": "3.0.0",
+        "compressible": "~2.0.14",
+        "debug": "2.6.9",
+        "on-headers": "~1.0.1",
+        "safe-buffer": "5.1.2",
+        "vary": "~1.1.2"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "concat-stream": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+      "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+      "dev": true,
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.2.2",
+        "typedarray": "^0.0.6"
+      },
+      "dependencies": {
+        "process-nextick-args": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+          "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        }
+      }
+    },
+    "connect": {
+      "version": "3.6.6",
+      "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz",
+      "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "finalhandler": "1.1.0",
+        "parseurl": "~1.3.2",
+        "utils-merge": "1.0.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "connect-history-api-fallback": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz",
+      "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
+      "dev": true
+    },
+    "console-browserify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
+      "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
+      "dev": true,
+      "requires": {
+        "date-now": "^0.1.4"
+      }
+    },
+    "console-control-strings": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+      "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
+      "dev": true
+    },
+    "constantinople": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz",
+      "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==",
+      "dev": true,
+      "requires": {
+        "@types/babel-types": "^7.0.0",
+        "@types/babylon": "^6.16.2",
+        "babel-types": "^6.26.0",
+        "babylon": "^6.18.0"
+      }
+    },
+    "constants-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+      "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
+      "dev": true
+    },
+    "content-disposition": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+      "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=",
+      "dev": true
+    },
+    "content-type": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+      "dev": true
+    },
+    "convert-source-map": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
+      "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "~5.1.1"
+      }
+    },
+    "cookie": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
+      "dev": true
+    },
+    "cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
+      "dev": true
+    },
+    "copy-concurrently": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz",
+      "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==",
+      "dev": true,
+      "requires": {
+        "aproba": "^1.1.1",
+        "fs-write-stream-atomic": "^1.0.8",
+        "iferr": "^0.1.5",
+        "mkdirp": "^0.5.1",
+        "rimraf": "^2.5.4",
+        "run-queue": "^1.0.0"
+      }
+    },
+    "copy-descriptor": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+      "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
+      "dev": true
+    },
+    "copy-webpack-plugin": {
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.5.2.tgz",
+      "integrity": "sha512-zmC33E8FFSq3AbflTvqvPvBo621H36Afsxlui91d+QyZxPIuXghfnTsa1CuqiAaCPgJoSUWfTFbKJnadZpKEbQ==",
+      "dev": true,
+      "requires": {
+        "cacache": "^10.0.4",
+        "find-cache-dir": "^1.0.0",
+        "globby": "^7.1.1",
+        "is-glob": "^4.0.0",
+        "loader-utils": "^1.1.0",
+        "minimatch": "^3.0.4",
+        "p-limit": "^1.0.0",
+        "serialize-javascript": "^1.4.0"
+      },
+      "dependencies": {
+        "globby": {
+          "version": "7.1.1",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz",
+          "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=",
+          "dev": true,
+          "requires": {
+            "array-union": "^1.0.1",
+            "dir-glob": "^2.0.0",
+            "glob": "^7.1.2",
+            "ignore": "^3.3.5",
+            "pify": "^3.0.0",
+            "slash": "^1.0.0"
+          }
+        }
+      }
+    },
+    "core-js": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.2.tgz",
+      "integrity": "sha512-NdBPF/RVwPW6jr0NCILuyN9RiqLo2b1mddWHkUL+VnvcB7dzlnBJ1bXYntjpTGOgkZiiLWj2JxmOr7eGE3qK6g==",
+      "dev": true
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+    },
+    "create-ecdh": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
+      "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "elliptic": "^6.0.0"
+      }
+    },
+    "create-hash": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+      "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "^1.0.1",
+        "inherits": "^2.0.1",
+        "md5.js": "^1.3.4",
+        "ripemd160": "^2.0.1",
+        "sha.js": "^2.4.0"
+      }
+    },
+    "create-hmac": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+      "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "^1.0.3",
+        "create-hash": "^1.1.0",
+        "inherits": "^2.0.1",
+        "ripemd160": "^2.0.0",
+        "safe-buffer": "^5.0.1",
+        "sha.js": "^2.4.8"
+      }
+    },
+    "cross-spawn": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
+      "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
+      "dev": true,
+      "requires": {
+        "lru-cache": "^4.0.1",
+        "shebang-command": "^1.2.0",
+        "which": "^1.2.9"
+      }
+    },
+    "crypto-browserify": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+      "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+      "dev": true,
+      "requires": {
+        "browserify-cipher": "^1.0.0",
+        "browserify-sign": "^4.0.0",
+        "create-ecdh": "^4.0.0",
+        "create-hash": "^1.1.0",
+        "create-hmac": "^1.1.0",
+        "diffie-hellman": "^5.0.0",
+        "inherits": "^2.0.1",
+        "pbkdf2": "^3.0.3",
+        "public-encrypt": "^4.0.0",
+        "randombytes": "^2.0.0",
+        "randomfill": "^1.0.3"
+      }
+    },
+    "css": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
+      "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.3",
+        "source-map": "^0.6.1",
+        "source-map-resolve": "^0.5.2",
+        "urix": "^0.1.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "css-color-names": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
+      "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=",
+      "dev": true
+    },
+    "css-loader": {
+      "version": "0.28.7",
+      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.7.tgz",
+      "integrity": "sha512-GxMpax8a/VgcfRrVy0gXD6yLd5ePYbXX/5zGgTVYp4wXtJklS8Z2VaUArJgc//f6/Dzil7BaJObdSv8eKKCPgg==",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "^6.11.0",
+        "css-selector-tokenizer": "^0.7.0",
+        "cssnano": ">=2.6.1 <4",
+        "icss-utils": "^2.1.0",
+        "loader-utils": "^1.0.2",
+        "lodash.camelcase": "^4.3.0",
+        "object-assign": "^4.0.1",
+        "postcss": "^5.0.6",
+        "postcss-modules-extract-imports": "^1.0.0",
+        "postcss-modules-local-by-default": "^1.0.1",
+        "postcss-modules-scope": "^1.0.0",
+        "postcss-modules-values": "^1.1.0",
+        "postcss-value-parser": "^3.3.0",
+        "source-list-map": "^2.0.0"
+      }
+    },
+    "css-select": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz",
+      "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==",
+      "dev": true,
+      "requires": {
+        "boolbase": "^1.0.0",
+        "css-what": "^2.1.2",
+        "domutils": "^1.7.0",
+        "nth-check": "^1.0.2"
+      }
+    },
+    "css-select-base-adapter": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
+      "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
+      "dev": true
+    },
+    "css-selector-tokenizer": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz",
+      "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==",
+      "dev": true,
+      "requires": {
+        "cssesc": "^0.1.0",
+        "fastparse": "^1.1.1",
+        "regexpu-core": "^1.0.0"
+      },
+      "dependencies": {
+        "jsesc": {
+          "version": "0.5.0",
+          "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+          "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
+          "dev": true
+        },
+        "regexpu-core": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz",
+          "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=",
+          "dev": true,
+          "requires": {
+            "regenerate": "^1.2.1",
+            "regjsgen": "^0.2.0",
+            "regjsparser": "^0.1.4"
+          }
+        },
+        "regjsgen": {
+          "version": "0.2.0",
+          "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
+          "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=",
+          "dev": true
+        },
+        "regjsparser": {
+          "version": "0.1.5",
+          "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
+          "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
+          "dev": true,
+          "requires": {
+            "jsesc": "~0.5.0"
+          }
+        }
+      }
+    },
+    "css-tree": {
+      "version": "1.0.0-alpha.28",
+      "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.28.tgz",
+      "integrity": "sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w==",
+      "dev": true,
+      "requires": {
+        "mdn-data": "~1.1.0",
+        "source-map": "^0.5.3"
+      }
+    },
+    "css-url-regex": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/css-url-regex/-/css-url-regex-1.1.0.tgz",
+      "integrity": "sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=",
+      "dev": true
+    },
+    "css-what": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.2.tgz",
+      "integrity": "sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ==",
+      "dev": true
+    },
+    "cssesc": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz",
+      "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=",
+      "dev": true
+    },
+    "cssnano": {
+      "version": "3.10.0",
+      "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.10.0.tgz",
+      "integrity": "sha1-Tzj2zqK5sX+gFJDyPx3GjqZcHDg=",
+      "dev": true,
+      "requires": {
+        "autoprefixer": "^6.3.1",
+        "decamelize": "^1.1.2",
+        "defined": "^1.0.0",
+        "has": "^1.0.1",
+        "object-assign": "^4.0.1",
+        "postcss": "^5.0.14",
+        "postcss-calc": "^5.2.0",
+        "postcss-colormin": "^2.1.8",
+        "postcss-convert-values": "^2.3.4",
+        "postcss-discard-comments": "^2.0.4",
+        "postcss-discard-duplicates": "^2.0.1",
+        "postcss-discard-empty": "^2.0.1",
+        "postcss-discard-overridden": "^0.1.1",
+        "postcss-discard-unused": "^2.2.1",
+        "postcss-filter-plugins": "^2.0.0",
+        "postcss-merge-idents": "^2.1.5",
+        "postcss-merge-longhand": "^2.0.1",
+        "postcss-merge-rules": "^2.0.3",
+        "postcss-minify-font-values": "^1.0.2",
+        "postcss-minify-gradients": "^1.0.1",
+        "postcss-minify-params": "^1.0.4",
+        "postcss-minify-selectors": "^2.0.4",
+        "postcss-normalize-charset": "^1.1.0",
+        "postcss-normalize-url": "^3.0.7",
+        "postcss-ordered-values": "^2.1.0",
+        "postcss-reduce-idents": "^2.2.2",
+        "postcss-reduce-initial": "^1.0.0",
+        "postcss-reduce-transforms": "^1.0.3",
+        "postcss-svgo": "^2.1.1",
+        "postcss-unique-selectors": "^2.0.2",
+        "postcss-value-parser": "^3.2.3",
+        "postcss-zindex": "^2.0.1"
+      }
+    },
+    "csso": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz",
+      "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=",
+      "dev": true,
+      "requires": {
+        "clap": "^1.0.9",
+        "source-map": "^0.5.3"
+      }
+    },
+    "currently-unhandled": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+      "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+      "dev": true,
+      "requires": {
+        "array-find-index": "^1.0.1"
+      }
+    },
+    "custom-event": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
+      "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=",
+      "dev": true
+    },
+    "cyclist": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz",
+      "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=",
+      "dev": true
+    },
+    "d3": {
+      "version": "3.5.17",
+      "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz",
+      "integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g="
+    },
+    "dashdash": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "date-format": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/date-format/-/date-format-1.2.0.tgz",
+      "integrity": "sha1-YV6CjiM90aubua4JUODOzPpuytg=",
+      "dev": true
+    },
+    "date-now": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
+      "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=",
+      "dev": true
+    },
+    "debug": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+      "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+      "requires": {
+        "ms": "^2.1.1"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+      "dev": true
+    },
+    "decode-uri-component": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+      "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+      "dev": true
+    },
+    "deep-eql": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-2.0.2.tgz",
+      "integrity": "sha1-sbrAblbwp2d3aG1Qyf63XC7XZ5o=",
+      "dev": true,
+      "requires": {
+        "type-detect": "^3.0.0"
+      },
+      "dependencies": {
+        "type-detect": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-3.0.0.tgz",
+          "integrity": "sha1-RtDMhVOrt7E6NSsNbeov1Y8tm1U=",
+          "dev": true
+        }
+      }
+    },
+    "deep-equal": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
+      "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
+      "dev": true
+    },
+    "deep-is": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+      "dev": true
+    },
+    "deepmerge": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.3.2.tgz",
+      "integrity": "sha1-FmNpFinU2/42T6EqKk8KqGqjoFA=",
+      "dev": true
+    },
+    "default-gateway": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.0.1.tgz",
+      "integrity": "sha512-JnSsMUgrBFy9ycs+tmOvLHN1GpILe+hNSUrIVM8mXjymfcBH9a7LJjOdoHLuUqKGuCUk6mSIPJjZ11Zszrg3oQ==",
+      "dev": true,
+      "requires": {
+        "execa": "^1.0.0",
+        "ip-regex": "^2.1.0"
+      }
+    },
+    "define-properties": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+      "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+      "dev": true,
+      "requires": {
+        "object-keys": "^1.0.12"
+      }
+    },
+    "define-property": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+      "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+      "dev": true,
+      "requires": {
+        "is-descriptor": "^1.0.2",
+        "isobject": "^3.0.1"
+      },
+      "dependencies": {
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "defined": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+      "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
+      "dev": true
+    },
+    "del": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz",
+      "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=",
+      "dev": true,
+      "requires": {
+        "globby": "^6.1.0",
+        "is-path-cwd": "^1.0.0",
+        "is-path-in-cwd": "^1.0.0",
+        "p-map": "^1.1.1",
+        "pify": "^3.0.0",
+        "rimraf": "^2.2.8"
+      },
+      "dependencies": {
+        "globby": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
+          "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
+          "dev": true,
+          "requires": {
+            "array-union": "^1.0.1",
+            "glob": "^7.0.3",
+            "object-assign": "^4.0.1",
+            "pify": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          },
+          "dependencies": {
+            "pify": {
+              "version": "2.3.0",
+              "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+              "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+              "dev": true
+            }
+          }
+        }
+      }
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+      "dev": true
+    },
+    "delegates": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+      "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
+      "dev": true
+    },
+    "depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
+      "dev": true
+    },
+    "des.js": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
+      "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
+    "destroy": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+      "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
+      "dev": true
+    },
+    "detect-file": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+      "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
+      "dev": true
+    },
+    "detect-node": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz",
+      "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
+      "dev": true
+    },
+    "di": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
+      "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=",
+      "dev": true
+    },
+    "diff": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz",
+      "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=",
+      "dev": true
+    },
+    "diffie-hellman": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+      "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "miller-rabin": "^4.0.0",
+        "randombytes": "^2.0.0"
+      }
+    },
+    "dir-glob": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.1.tgz",
+      "integrity": "sha512-UN6X6XwRjllabfRhBdkVSo63uurJ8nSvMGrwl94EYVz6g+exhTV+yVSYk5VC/xl3MBFBTtC0J20uFKce4Brrng==",
+      "dev": true,
+      "requires": {
+        "path-type": "^3.0.0"
+      }
+    },
+    "dns-equal": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
+      "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=",
+      "dev": true
+    },
+    "dns-packet": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz",
+      "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==",
+      "dev": true,
+      "requires": {
+        "ip": "^1.1.0",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "dns-txt": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz",
+      "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=",
+      "dev": true,
+      "requires": {
+        "buffer-indexof": "^1.0.0"
+      }
+    },
+    "doctrine": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+      "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2"
+      }
+    },
+    "doctypes": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
+      "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=",
+      "dev": true
+    },
+    "dom-converter": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
+      "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==",
+      "dev": true,
+      "requires": {
+        "utila": "~0.4"
+      }
+    },
+    "dom-serialize": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
+      "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=",
+      "dev": true,
+      "requires": {
+        "custom-event": "~1.0.0",
+        "ent": "~2.2.0",
+        "extend": "^3.0.0",
+        "void-elements": "^2.0.0"
+      }
+    },
+    "dom-serializer": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
+      "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
+      "dev": true,
+      "requires": {
+        "domelementtype": "~1.1.1",
+        "entities": "~1.1.1"
+      },
+      "dependencies": {
+        "domelementtype": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
+          "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=",
+          "dev": true
+        }
+      }
+    },
+    "domain-browser": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz",
+      "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=",
+      "dev": true
+    },
+    "domelementtype": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
+      "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
+      "dev": true
+    },
+    "domhandler": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
+      "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
+      "dev": true,
+      "requires": {
+        "domelementtype": "1"
+      }
+    },
+    "domready": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/domready/-/domready-1.0.8.tgz",
+      "integrity": "sha1-kfJS5Ze2Wvd+dFriTdAYXV4m1Yw=",
+      "dev": true
+    },
+    "domutils": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
+      "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
+      "dev": true,
+      "requires": {
+        "dom-serializer": "0",
+        "domelementtype": "1"
+      }
+    },
+    "duplexify": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz",
+      "integrity": "sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA==",
+      "dev": true,
+      "requires": {
+        "end-of-stream": "^1.0.0",
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.0",
+        "stream-shift": "^1.0.0"
+      }
+    },
+    "ecc-jsbn": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+      "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+      "dev": true,
+      "requires": {
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
+    "ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
+      "dev": true
+    },
+    "electron-to-chromium": {
+      "version": "1.3.100",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.100.tgz",
+      "integrity": "sha512-cEUzis2g/RatrVf8x26L8lK5VEls1AGnLHk6msluBUg/NTB4wcXzExTsGscFq+Vs4WBBU2zbLLySvD4C0C3hwg==",
+      "dev": true
+    },
+    "elliptic": {
+      "version": "6.4.1",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
+      "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.4.0",
+        "brorand": "^1.0.1",
+        "hash.js": "^1.0.0",
+        "hmac-drbg": "^1.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.0"
+      }
+    },
+    "emojis-list": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
+      "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
+      "dev": true
+    },
+    "encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
+      "dev": true
+    },
+    "encoding": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
+      "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
+      "dev": true,
+      "requires": {
+        "iconv-lite": "~0.4.13"
+      }
+    },
+    "end-of-stream": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
+      "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
+      "dev": true,
+      "requires": {
+        "once": "^1.4.0"
+      }
+    },
+    "engine.io": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz",
+      "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==",
+      "dev": true,
+      "requires": {
+        "accepts": "~1.3.4",
+        "base64id": "1.0.0",
+        "cookie": "0.3.1",
+        "debug": "~3.1.0",
+        "engine.io-parser": "~2.1.0",
+        "ws": "~3.3.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "engine.io-client": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
+      "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==",
+      "requires": {
+        "component-emitter": "1.2.1",
+        "component-inherit": "0.0.3",
+        "debug": "~3.1.0",
+        "engine.io-parser": "~2.1.1",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "ws": "~3.3.1",
+        "xmlhttprequest-ssl": "~1.5.4",
+        "yeast": "0.1.2"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        }
+      }
+    },
+    "engine.io-parser": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz",
+      "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==",
+      "requires": {
+        "after": "0.8.2",
+        "arraybuffer.slice": "~0.0.7",
+        "base64-arraybuffer": "0.1.5",
+        "blob": "0.0.5",
+        "has-binary2": "~1.0.2"
+      }
+    },
+    "enhanced-resolve": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz",
+      "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "memory-fs": "^0.4.0",
+        "tapable": "^1.0.0"
+      }
+    },
+    "ent": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
+      "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=",
+      "dev": true
+    },
+    "entities": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
+      "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
+      "dev": true
+    },
+    "errno": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
+      "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
+      "dev": true,
+      "requires": {
+        "prr": "~1.0.1"
+      }
+    },
+    "error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "dev": true,
+      "requires": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "es-abstract": {
+      "version": "1.13.0",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz",
+      "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==",
+      "dev": true,
+      "requires": {
+        "es-to-primitive": "^1.2.0",
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3",
+        "is-callable": "^1.1.4",
+        "is-regex": "^1.0.4",
+        "object-keys": "^1.0.12"
+      }
+    },
+    "es-to-primitive": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz",
+      "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==",
+      "dev": true,
+      "requires": {
+        "is-callable": "^1.1.4",
+        "is-date-object": "^1.0.1",
+        "is-symbol": "^1.0.2"
+      }
+    },
+    "es6-promise": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz",
+      "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y="
+    },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
+      "dev": true
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+    },
+    "eslint": {
+      "version": "5.12.1",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.12.1.tgz",
+      "integrity": "sha512-54NV+JkTpTu0d8+UYSA8mMKAG4XAsaOrozA9rCW7tgneg1mevcL7wIotPC+fZ0SkWwdhNqoXoxnQCTBp7UvTsg==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "ajv": "^6.5.3",
+        "chalk": "^2.1.0",
+        "cross-spawn": "^6.0.5",
+        "debug": "^4.0.1",
+        "doctrine": "^2.1.0",
+        "eslint-scope": "^4.0.0",
+        "eslint-utils": "^1.3.1",
+        "eslint-visitor-keys": "^1.0.0",
+        "espree": "^5.0.0",
+        "esquery": "^1.0.1",
+        "esutils": "^2.0.2",
+        "file-entry-cache": "^2.0.0",
+        "functional-red-black-tree": "^1.0.1",
+        "glob": "^7.1.2",
+        "globals": "^11.7.0",
+        "ignore": "^4.0.6",
+        "import-fresh": "^3.0.0",
+        "imurmurhash": "^0.1.4",
+        "inquirer": "^6.1.0",
+        "js-yaml": "^3.12.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.3.0",
+        "lodash": "^4.17.5",
+        "minimatch": "^3.0.4",
+        "mkdirp": "^0.5.1",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.8.2",
+        "path-is-inside": "^1.0.2",
+        "pluralize": "^7.0.0",
+        "progress": "^2.0.0",
+        "regexpp": "^2.0.1",
+        "semver": "^5.5.1",
+        "strip-ansi": "^4.0.0",
+        "strip-json-comments": "^2.0.1",
+        "table": "^5.0.2",
+        "text-table": "^0.2.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.7.0",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz",
+          "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^2.0.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "cross-spawn": {
+          "version": "6.0.5",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+          "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+          "dev": true,
+          "requires": {
+            "nice-try": "^1.0.4",
+            "path-key": "^2.0.1",
+            "semver": "^5.5.0",
+            "shebang-command": "^1.2.0",
+            "which": "^1.2.9"
+          }
+        },
+        "esprima": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+          "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+          "dev": true
+        },
+        "fast-deep-equal": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+          "dev": true
+        },
+        "ignore": {
+          "version": "4.0.6",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+          "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+          "dev": true
+        },
+        "js-yaml": {
+          "version": "3.12.1",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz",
+          "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==",
+          "dev": true,
+          "requires": {
+            "argparse": "^1.0.7",
+            "esprima": "^4.0.0"
+          }
+        },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        }
+      }
+    },
+    "eslint-formatter-friendly": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-formatter-friendly/-/eslint-formatter-friendly-6.0.0.tgz",
+      "integrity": "sha512-fOBwGn2r8BPQ1KSKyVzjXP8VFxJ2tWKxxn2lIF+k1ezN/pFB44HDlrn5kBm1vxbyyRa/LC+1vHJwc7WETUAZ2Q==",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "6.26.0",
+        "chalk": "^2.0.1",
+        "extend": "^3.0.0",
+        "strip-ansi": "^4.0.0",
+        "text-table": "^0.2.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        }
+      }
+    },
+    "eslint-loader": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-2.1.0.tgz",
+      "integrity": "sha512-f4A/Yk7qF+HcFSz5Tck2QoKIwJVHlX0soJk5MkROYahb5uvspad5Ba60rrz4u/V2/MEj1dtp/uBi6LlLWVaY7Q==",
+      "dev": true,
+      "requires": {
+        "loader-fs-cache": "^1.0.0",
+        "loader-utils": "^1.0.2",
+        "object-assign": "^4.0.1",
+        "object-hash": "^1.1.4",
+        "rimraf": "^2.6.1"
+      }
+    },
+    "eslint-scope": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz",
+      "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==",
+      "dev": true,
+      "requires": {
+        "esrecurse": "^4.1.0",
+        "estraverse": "^4.1.1"
+      }
+    },
+    "eslint-utils": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
+      "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==",
+      "dev": true
+    },
+    "eslint-visitor-keys": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
+      "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==",
+      "dev": true
+    },
+    "espree": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.0.tgz",
+      "integrity": "sha512-1MpUfwsdS9MMoN7ZXqAr9e9UKdVHDcvrJpyx7mm1WuQlx/ygErEQBzgi5Nh5qBHIoYweprhtMkTCb9GhcAIcsA==",
+      "dev": true,
+      "requires": {
+        "acorn": "^6.0.2",
+        "acorn-jsx": "^5.0.0",
+        "eslint-visitor-keys": "^1.0.0"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "6.0.5",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.5.tgz",
+          "integrity": "sha512-i33Zgp3XWtmZBMNvCr4azvOFeWVw1Rk6p3hfi3LUDvIFraOMywb1kAtrbi+med14m4Xfpqm3zRZMT+c0FNE7kg==",
+          "dev": true
+        }
+      }
+    },
+    "esprima": {
+      "version": "2.7.3",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz",
+      "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=",
+      "dev": true
+    },
+    "esquery": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz",
+      "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.0.0"
+      }
+    },
+    "esrecurse": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+      "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.1.0"
+      }
+    },
+    "estraverse": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
+      "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
+      "dev": true
+    },
+    "esutils": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs="
+    },
+    "etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
+      "dev": true
+    },
+    "eventemitter3": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz",
+      "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==",
+      "dev": true
+    },
+    "events": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
+      "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=",
+      "dev": true
+    },
+    "eventsource": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz",
+      "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==",
+      "dev": true,
+      "requires": {
+        "original": "^1.0.0"
+      }
+    },
+    "evp_bytestokey": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+      "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+      "dev": true,
+      "requires": {
+        "md5.js": "^1.3.4",
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "execa": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+      "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
+      "dev": true,
+      "requires": {
+        "cross-spawn": "^6.0.0",
+        "get-stream": "^4.0.0",
+        "is-stream": "^1.1.0",
+        "npm-run-path": "^2.0.0",
+        "p-finally": "^1.0.0",
+        "signal-exit": "^3.0.0",
+        "strip-eof": "^1.0.0"
+      },
+      "dependencies": {
+        "cross-spawn": {
+          "version": "6.0.5",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+          "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+          "dev": true,
+          "requires": {
+            "nice-try": "^1.0.4",
+            "path-key": "^2.0.1",
+            "semver": "^5.5.0",
+            "shebang-command": "^1.2.0",
+            "which": "^1.2.9"
+          }
+        }
+      }
+    },
+    "expand-braces": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz",
+      "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=",
+      "dev": true,
+      "requires": {
+        "array-slice": "^0.2.3",
+        "array-unique": "^0.2.1",
+        "braces": "^0.1.2"
+      },
+      "dependencies": {
+        "array-unique": {
+          "version": "0.2.1",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+          "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+          "dev": true
+        },
+        "braces": {
+          "version": "0.1.5",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz",
+          "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=",
+          "dev": true,
+          "requires": {
+            "expand-range": "^0.1.0"
+          }
+        }
+      }
+    },
+    "expand-brackets": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+      "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+      "dev": true,
+      "requires": {
+        "debug": "^2.3.3",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "posix-character-classes": "^0.1.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "expand-range": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz",
+      "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=",
+      "dev": true,
+      "requires": {
+        "is-number": "^0.1.1",
+        "repeat-string": "^0.2.2"
+      },
+      "dependencies": {
+        "is-number": {
+          "version": "0.1.1",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz",
+          "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=",
+          "dev": true
+        },
+        "repeat-string": {
+          "version": "0.2.2",
+          "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz",
+          "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=",
+          "dev": true
+        }
+      }
+    },
+    "expand-tilde": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+      "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=",
+      "dev": true,
+      "requires": {
+        "homedir-polyfill": "^1.0.1"
+      }
+    },
+    "expose-loader": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-0.7.5.tgz",
+      "integrity": "sha512-iPowgKUZkTPX5PznYsmifVj9Bob0w2wTHVkt/eYNPSzyebkUgIedmskf/kcfEIWpiWjg3JRjnW+a17XypySMuw==",
+      "dev": true
+    },
+    "express": {
+      "version": "4.16.4",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
+      "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
+      "dev": true,
+      "requires": {
+        "accepts": "~1.3.5",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.18.3",
+        "content-disposition": "0.5.2",
+        "content-type": "~1.0.4",
+        "cookie": "0.3.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "1.1.1",
+        "fresh": "0.5.2",
+        "merge-descriptors": "1.0.1",
+        "methods": "~1.1.2",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.2",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "~2.0.4",
+        "qs": "6.5.2",
+        "range-parser": "~1.2.0",
+        "safe-buffer": "5.1.2",
+        "send": "0.16.2",
+        "serve-static": "1.13.2",
+        "setprototypeof": "1.1.0",
+        "statuses": "~1.4.0",
+        "type-is": "~1.6.16",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "dependencies": {
+        "array-flatten": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+          "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
+          "dev": true
+        },
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "finalhandler": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
+          "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
+          "dev": true,
+          "requires": {
+            "debug": "2.6.9",
+            "encodeurl": "~1.0.2",
+            "escape-html": "~1.0.3",
+            "on-finished": "~2.3.0",
+            "parseurl": "~1.3.2",
+            "statuses": "~1.4.0",
+            "unpipe": "~1.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        },
+        "path-to-regexp": {
+          "version": "0.1.7",
+          "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+          "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
+          "dev": true
+        },
+        "statuses": {
+          "version": "1.4.0",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+          "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==",
+          "dev": true
+        }
+      }
+    },
+    "extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "dev": true
+    },
+    "extend-shallow": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+      "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+      "dev": true,
+      "requires": {
+        "assign-symbols": "^1.0.0",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "dev": true,
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "external-editor": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz",
+      "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==",
+      "dev": true,
+      "requires": {
+        "chardet": "^0.7.0",
+        "iconv-lite": "^0.4.24",
+        "tmp": "^0.0.33"
+      }
+    },
+    "extglob": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+      "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+      "dev": true,
+      "requires": {
+        "array-unique": "^0.3.2",
+        "define-property": "^1.0.0",
+        "expand-brackets": "^2.1.4",
+        "extend-shallow": "^2.0.1",
+        "fragment-cache": "^0.2.1",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "extsprintf": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+      "dev": true
+    },
+    "fast-deep-equal": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+      "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
+      "dev": true
+    },
+    "fast-glob": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.6.tgz",
+      "integrity": "sha512-0BvMaZc1k9F+MeWWMe8pL6YltFzZYcJsYU7D4JyDA6PAczaXvxqQQ/z+mDF7/4Mw01DeUc+i3CTKajnkANkV4w==",
+      "dev": true,
+      "requires": {
+        "@mrmlnc/readdir-enhanced": "^2.2.1",
+        "@nodelib/fs.stat": "^1.1.2",
+        "glob-parent": "^3.1.0",
+        "is-glob": "^4.0.0",
+        "merge2": "^1.2.3",
+        "micromatch": "^3.1.10"
+      }
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+      "dev": true
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+      "dev": true
+    },
+    "fastparse": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
+      "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
+      "dev": true
+    },
+    "faye-websocket": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
+      "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=",
+      "dev": true,
+      "requires": {
+        "websocket-driver": ">=0.5.1"
+      }
+    },
+    "figures": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+      "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+      "dev": true,
+      "requires": {
+        "escape-string-regexp": "^1.0.5"
+      }
+    },
+    "file-entry-cache": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz",
+      "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=",
+      "dev": true,
+      "requires": {
+        "flat-cache": "^1.2.1",
+        "object-assign": "^4.0.1"
+      }
+    },
+    "file-loader": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz",
+      "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.0.2",
+        "schema-utils": "^0.4.5"
+      }
+    },
+    "file-saver": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-1.3.3.tgz",
+      "integrity": "sha1-zdTETTqiZOrC9o7BZbx5HDSvEjI="
+    },
+    "fill-range": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+      "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^2.0.1",
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1",
+        "to-regex-range": "^2.1.0"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "finalhandler": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
+      "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "encodeurl": "~1.0.1",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.2",
+        "statuses": "~1.3.1",
+        "unpipe": "~1.0.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        },
+        "statuses": {
+          "version": "1.3.1",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
+          "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=",
+          "dev": true
+        }
+      }
+    },
+    "find-cache-dir": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz",
+      "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=",
+      "dev": true,
+      "requires": {
+        "commondir": "^1.0.1",
+        "make-dir": "^1.0.0",
+        "pkg-dir": "^2.0.0"
+      }
+    },
+    "find-up": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+      "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+      "dev": true,
+      "requires": {
+        "locate-path": "^2.0.0"
+      }
+    },
+    "findup-sync": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
+      "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=",
+      "dev": true,
+      "requires": {
+        "detect-file": "^1.0.0",
+        "is-glob": "^3.1.0",
+        "micromatch": "^3.0.4",
+        "resolve-dir": "^1.0.1"
+      },
+      "dependencies": {
+        "is-glob": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+          "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^2.1.0"
+          }
+        }
+      }
+    },
+    "flat": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz",
+      "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==",
+      "dev": true,
+      "requires": {
+        "is-buffer": "~2.0.3"
+      },
+      "dependencies": {
+        "is-buffer": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
+          "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==",
+          "dev": true
+        }
+      }
+    },
+    "flat-cache": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz",
+      "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==",
+      "dev": true,
+      "requires": {
+        "circular-json": "^0.3.1",
+        "graceful-fs": "^4.1.2",
+        "rimraf": "~2.6.2",
+        "write": "^0.2.1"
+      }
+    },
+    "flatted": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz",
+      "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==",
+      "dev": true
+    },
+    "flatten": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz",
+      "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=",
+      "dev": true
+    },
+    "flush-write-stream": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
+      "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.4"
+      }
+    },
+    "follow-redirects": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz",
+      "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==",
+      "dev": true,
+      "requires": {
+        "debug": "^3.2.6"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
+      }
+    },
+    "font-awesome": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
+      "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM="
+    },
+    "for-in": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+      "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+      "dev": true
+    },
+    "forever-agent": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+      "dev": true
+    },
+    "form-data": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+      "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+      "dev": true,
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.6",
+        "mime-types": "^2.1.12"
+      }
+    },
+    "formatio": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz",
+      "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=",
+      "dev": true,
+      "requires": {
+        "samsam": "1.x"
+      }
+    },
+    "forwarded": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+      "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
+      "dev": true
+    },
+    "fragment-cache": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+      "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+      "dev": true,
+      "requires": {
+        "map-cache": "^0.2.2"
+      }
+    },
+    "fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
+      "dev": true
+    },
+    "from2": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
+      "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.0"
+      }
+    },
+    "fs-access": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz",
+      "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=",
+      "dev": true,
+      "requires": {
+        "null-check": "^1.0.0"
+      }
+    },
+    "fs-write-stream-atomic": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
+      "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "iferr": "^0.1.5",
+        "imurmurhash": "^0.1.4",
+        "readable-stream": "1 || 2"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "fsevents": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz",
+      "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "nan": "^2.9.2",
+        "node-pre-gyp": "^0.10.0"
+      },
+      "dependencies": {
+        "abbrev": {
+          "version": "1.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "ansi-regex": {
+          "version": "2.1.1",
+          "bundled": true,
+          "dev": true
+        },
+        "aproba": {
+          "version": "1.2.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "are-we-there-yet": {
+          "version": "1.1.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "delegates": "^1.0.0",
+            "readable-stream": "^2.0.6"
+          }
+        },
+        "balanced-match": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true
+        },
+        "brace-expansion": {
+          "version": "1.1.11",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "chownr": {
+          "version": "1.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "code-point-at": {
+          "version": "1.1.0",
+          "bundled": true,
+          "dev": true
+        },
+        "concat-map": {
+          "version": "0.0.1",
+          "bundled": true,
+          "dev": true
+        },
+        "console-control-strings": {
+          "version": "1.1.0",
+          "bundled": true,
+          "dev": true
+        },
+        "core-util-is": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "debug": {
+          "version": "2.6.9",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "deep-extend": {
+          "version": "0.6.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "delegates": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "detect-libc": {
+          "version": "1.0.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "fs-minipass": {
+          "version": "1.2.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minipass": "^2.2.1"
+          }
+        },
+        "fs.realpath": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "gauge": {
+          "version": "2.7.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "aproba": "^1.0.3",
+            "console-control-strings": "^1.0.0",
+            "has-unicode": "^2.0.0",
+            "object-assign": "^4.1.0",
+            "signal-exit": "^3.0.0",
+            "string-width": "^1.0.1",
+            "strip-ansi": "^3.0.1",
+            "wide-align": "^1.1.0"
+          }
+        },
+        "glob": {
+          "version": "7.1.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "has-unicode": {
+          "version": "2.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "iconv-lite": {
+          "version": "0.4.24",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3"
+          }
+        },
+        "ignore-walk": {
+          "version": "3.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minimatch": "^3.0.4"
+          }
+        },
+        "inflight": {
+          "version": "1.0.6",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "once": "^1.3.0",
+            "wrappy": "1"
+          }
+        },
+        "inherits": {
+          "version": "2.0.3",
+          "bundled": true,
+          "dev": true
+        },
+        "ini": {
+          "version": "1.3.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "isarray": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
+        "minimist": {
+          "version": "0.0.8",
+          "bundled": true,
+          "dev": true
+        },
+        "minipass": {
+          "version": "2.3.5",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "safe-buffer": "^5.1.2",
+            "yallist": "^3.0.0"
+          }
+        },
+        "minizlib": {
+          "version": "1.2.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minipass": "^2.2.1"
+          }
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "needle": {
+          "version": "2.2.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "debug": "^2.1.2",
+            "iconv-lite": "^0.4.4",
+            "sax": "^1.2.4"
+          }
+        },
+        "node-pre-gyp": {
+          "version": "0.10.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "detect-libc": "^1.0.2",
+            "mkdirp": "^0.5.1",
+            "needle": "^2.2.1",
+            "nopt": "^4.0.1",
+            "npm-packlist": "^1.1.6",
+            "npmlog": "^4.0.2",
+            "rc": "^1.2.7",
+            "rimraf": "^2.6.1",
+            "semver": "^5.3.0",
+            "tar": "^4"
+          }
+        },
+        "nopt": {
+          "version": "4.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "abbrev": "1",
+            "osenv": "^0.1.4"
+          }
+        },
+        "npm-bundled": {
+          "version": "1.0.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "npm-packlist": {
+          "version": "1.2.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ignore-walk": "^3.0.1",
+            "npm-bundled": "^1.0.1"
+          }
+        },
+        "npmlog": {
+          "version": "4.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "are-we-there-yet": "~1.1.2",
+            "console-control-strings": "~1.1.0",
+            "gauge": "~2.7.3",
+            "set-blocking": "~2.0.0"
+          }
+        },
+        "number-is-nan": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true
+        },
+        "object-assign": {
+          "version": "4.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "once": {
+          "version": "1.4.0",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "wrappy": "1"
+          }
+        },
+        "os-homedir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "os-tmpdir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "osenv": {
+          "version": "0.1.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "os-homedir": "^1.0.0",
+            "os-tmpdir": "^1.0.0"
+          }
+        },
+        "path-is-absolute": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "process-nextick-args": {
+          "version": "2.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "rc": {
+          "version": "1.2.8",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "deep-extend": "^0.6.0",
+            "ini": "~1.3.0",
+            "minimist": "^1.2.0",
+            "strip-json-comments": "~2.0.1"
+          },
+          "dependencies": {
+            "minimist": {
+              "version": "1.2.0",
+              "bundled": true,
+              "dev": true,
+              "optional": true
+            }
+          }
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "rimraf": {
+          "version": "2.6.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "glob": "^7.1.3"
+          }
+        },
+        "safe-buffer": {
+          "version": "5.1.2",
+          "bundled": true,
+          "dev": true
+        },
+        "safer-buffer": {
+          "version": "2.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "sax": {
+          "version": "1.2.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "semver": {
+          "version": "5.6.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "set-blocking": {
+          "version": "2.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "signal-exit": {
+          "version": "3.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "strip-json-comments": {
+          "version": "2.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "tar": {
+          "version": "4.4.8",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "chownr": "^1.1.1",
+            "fs-minipass": "^1.2.5",
+            "minipass": "^2.3.4",
+            "minizlib": "^1.1.1",
+            "mkdirp": "^0.5.0",
+            "safe-buffer": "^5.1.2",
+            "yallist": "^3.0.2"
+          }
+        },
+        "util-deprecate": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "wide-align": {
+          "version": "1.1.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "string-width": "^1.0.2 || 2"
+          }
+        },
+        "wrappy": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true
+        },
+        "yallist": {
+          "version": "3.0.3",
+          "bundled": true,
+          "dev": true
+        }
+      }
+    },
+    "fstream": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
+      "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "inherits": "~2.0.0",
+        "mkdirp": ">=0.5 0",
+        "rimraf": "2"
+      }
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "dev": true
+    },
+    "functional-red-black-tree": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+      "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+      "dev": true
+    },
+    "gauge": {
+      "version": "2.7.4",
+      "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+      "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+      "dev": true,
+      "requires": {
+        "aproba": "^1.0.3",
+        "console-control-strings": "^1.0.0",
+        "has-unicode": "^2.0.0",
+        "object-assign": "^4.1.0",
+        "signal-exit": "^3.0.0",
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.1",
+        "wide-align": "^1.1.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+          "dev": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "dev": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        }
+      }
+    },
+    "gaze": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
+      "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==",
+      "dev": true,
+      "requires": {
+        "globule": "^1.0.0"
+      }
+    },
+    "get-caller-file": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+      "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+      "dev": true
+    },
+    "get-func-name": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+      "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
+      "dev": true
+    },
+    "get-stdin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+      "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
+      "dev": true
+    },
+    "get-stream": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+      "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+      "dev": true,
+      "requires": {
+        "pump": "^3.0.0"
+      },
+      "dependencies": {
+        "pump": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+          "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+          "dev": true,
+          "requires": {
+            "end-of-stream": "^1.1.0",
+            "once": "^1.3.1"
+          }
+        }
+      }
+    },
+    "get-value": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+      "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
+      "dev": true
+    },
+    "getpass": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "glob": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+      "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "glob-parent": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+      "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+      "dev": true,
+      "requires": {
+        "is-glob": "^3.1.0",
+        "path-dirname": "^1.0.0"
+      },
+      "dependencies": {
+        "is-glob": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+          "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^2.1.0"
+          }
+        }
+      }
+    },
+    "glob-to-regexp": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz",
+      "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=",
+      "dev": true
+    },
+    "global-modules": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+      "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+      "dev": true,
+      "requires": {
+        "global-prefix": "^1.0.1",
+        "is-windows": "^1.0.1",
+        "resolve-dir": "^1.0.0"
+      }
+    },
+    "global-modules-path": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/global-modules-path/-/global-modules-path-2.3.1.tgz",
+      "integrity": "sha512-y+shkf4InI7mPRHSo2b/k6ix6+NLDtyccYv86whhxrSGX9wjPX1VMITmrDbE1eh7zkzhiWtW2sHklJYoQ62Cxg==",
+      "dev": true
+    },
+    "global-prefix": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+      "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
+      "dev": true,
+      "requires": {
+        "expand-tilde": "^2.0.2",
+        "homedir-polyfill": "^1.0.1",
+        "ini": "^1.3.4",
+        "is-windows": "^1.0.1",
+        "which": "^1.2.14"
+      }
+    },
+    "globals": {
+      "version": "11.10.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-11.10.0.tgz",
+      "integrity": "sha512-0GZF1RiPKU97IHUO5TORo9w1PwrH/NBPl+fS7oMLdaTRiYmYbwK4NWoZWrAdd0/abG9R2BU+OiwyQpTpE6pdfQ=="
+    },
+    "globby": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.1.tgz",
+      "integrity": "sha512-oMrYrJERnKBLXNLVTqhm3vPEdJ/b2ZE28xN4YARiix1NOIOBPEpOUnm844K1iu/BkphCaf2WNFwMszv8Soi1pw==",
+      "dev": true,
+      "requires": {
+        "array-union": "^1.0.1",
+        "dir-glob": "^2.0.0",
+        "fast-glob": "^2.0.2",
+        "glob": "^7.1.2",
+        "ignore": "^3.3.5",
+        "pify": "^3.0.0",
+        "slash": "^1.0.0"
+      }
+    },
+    "globule": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
+      "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==",
+      "dev": true,
+      "requires": {
+        "glob": "~7.1.1",
+        "lodash": "~4.17.10",
+        "minimatch": "~3.0.2"
+      }
+    },
+    "graceful-fs": {
+      "version": "4.1.15",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
+      "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
+      "dev": true
+    },
+    "growl": {
+      "version": "1.10.5",
+      "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+      "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+      "dev": true
+    },
+    "handle-thing": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz",
+      "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==",
+      "dev": true
+    },
+    "har-schema": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
+      "dev": true
+    },
+    "har-validator": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
+      "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.5.5",
+        "har-schema": "^2.0.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.6.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz",
+          "integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^2.0.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "fast-deep-equal": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+          "dev": true
+        },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        }
+      }
+    },
+    "has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dev": true,
+      "requires": {
+        "function-bind": "^1.1.1"
+      }
+    },
+    "has-ansi": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz",
+      "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=",
+      "requires": {
+        "ansi-regex": "^0.2.0"
+      }
+    },
+    "has-binary2": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
+      "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
+      "requires": {
+        "isarray": "2.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+        }
+      }
+    },
+    "has-cors": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+      "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+      "dev": true
+    },
+    "has-symbols": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
+      "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
+      "dev": true
+    },
+    "has-unicode": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+      "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
+      "dev": true
+    },
+    "has-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+      "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+      "dev": true,
+      "requires": {
+        "get-value": "^2.0.6",
+        "has-values": "^1.0.0",
+        "isobject": "^3.0.0"
+      }
+    },
+    "has-values": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+      "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+      "dev": true,
+      "requires": {
+        "is-number": "^3.0.0",
+        "kind-of": "^4.0.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+          "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "hash-base": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
+      "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "hash.js": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
+    "he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true
+    },
+    "hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+      "dev": true,
+      "requires": {
+        "hash.js": "^1.0.3",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "homedir-polyfill": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+      "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+      "dev": true,
+      "requires": {
+        "parse-passwd": "^1.0.0"
+      }
+    },
+    "hosted-git-info": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
+      "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
+      "dev": true
+    },
+    "hpack.js": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
+      "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "obuf": "^1.0.0",
+        "readable-stream": "^2.0.1",
+        "wbuf": "^1.1.0"
+      }
+    },
+    "html-comment-regex": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz",
+      "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==",
+      "dev": true
+    },
+    "html-entities": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
+      "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=",
+      "dev": true
+    },
+    "html-loader": {
+      "version": "1.0.0-alpha.0",
+      "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-1.0.0-alpha.0.tgz",
+      "integrity": "sha512-KcuaIRWTU0kFjOJCs32a3JsGNCWkeOak0/F/uvJNp3x/N4McXdqHpcK64cYTozK7QLPKKtUqb9h7wR9K9rYRkg==",
+      "dev": true,
+      "requires": {
+        "@posthtml/esm": "^1.0.0",
+        "htmlnano": "^0.1.6",
+        "loader-utils": "^1.1.0",
+        "posthtml": "^0.11.2",
+        "schema-utils": "^0.4.3"
+      }
+    },
+    "html-minifier": {
+      "version": "3.5.21",
+      "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz",
+      "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==",
+      "dev": true,
+      "requires": {
+        "camel-case": "3.0.x",
+        "clean-css": "4.2.x",
+        "commander": "2.17.x",
+        "he": "1.2.x",
+        "param-case": "2.1.x",
+        "relateurl": "0.2.x",
+        "uglify-js": "3.4.x"
+      }
+    },
+    "html-webpack-plugin": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz",
+      "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=",
+      "dev": true,
+      "requires": {
+        "html-minifier": "^3.2.3",
+        "loader-utils": "^0.2.16",
+        "lodash": "^4.17.3",
+        "pretty-error": "^2.0.2",
+        "tapable": "^1.0.0",
+        "toposort": "^1.0.0",
+        "util.promisify": "1.0.0"
+      },
+      "dependencies": {
+        "big.js": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
+          "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==",
+          "dev": true
+        },
+        "loader-utils": {
+          "version": "0.2.17",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz",
+          "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=",
+          "dev": true,
+          "requires": {
+            "big.js": "^3.1.3",
+            "emojis-list": "^2.0.0",
+            "json5": "^0.5.0",
+            "object-assign": "^4.0.1"
+          }
+        }
+      }
+    },
+    "htmlnano": {
+      "version": "0.1.10",
+      "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-0.1.10.tgz",
+      "integrity": "sha512-eTEUzz8VdWYp+w/KUdb99kwao4reR64epUySyZkQeepcyzPQ2n2EPWzibf6QDxmkGy10Kr+CKxYqI3izSbmhJQ==",
+      "dev": true,
+      "requires": {
+        "cssnano": "^3.4.0",
+        "object-assign": "^4.0.1",
+        "posthtml": "^0.11.3",
+        "posthtml-render": "^1.1.4",
+        "svgo": "^1.0.5",
+        "terser": "^3.8.1"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "coa": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
+          "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==",
+          "dev": true,
+          "requires": {
+            "@types/q": "^1.5.1",
+            "chalk": "^2.4.1",
+            "q": "^1.1.2"
+          }
+        },
+        "csso": {
+          "version": "3.5.1",
+          "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz",
+          "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==",
+          "dev": true,
+          "requires": {
+            "css-tree": "1.0.0-alpha.29"
+          },
+          "dependencies": {
+            "css-tree": {
+              "version": "1.0.0-alpha.29",
+              "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz",
+              "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==",
+              "dev": true,
+              "requires": {
+                "mdn-data": "~1.1.0",
+                "source-map": "^0.5.3"
+              }
+            }
+          }
+        },
+        "esprima": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+          "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+          "dev": true
+        },
+        "js-yaml": {
+          "version": "3.12.1",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz",
+          "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==",
+          "dev": true,
+          "requires": {
+            "argparse": "^1.0.7",
+            "esprima": "^4.0.0"
+          }
+        },
+        "svgo": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.1.1.tgz",
+          "integrity": "sha512-GBkJbnTuFpM4jFbiERHDWhZc/S/kpHToqmZag3aEBjPYK44JAN2QBjvrGIxLOoCyMZjuFQIfTO2eJd8uwLY/9g==",
+          "dev": true,
+          "requires": {
+            "coa": "~2.0.1",
+            "colors": "~1.1.2",
+            "css-select": "^2.0.0",
+            "css-select-base-adapter": "~0.1.0",
+            "css-tree": "1.0.0-alpha.28",
+            "css-url-regex": "^1.1.0",
+            "csso": "^3.5.0",
+            "js-yaml": "^3.12.0",
+            "mkdirp": "~0.5.1",
+            "object.values": "^1.0.4",
+            "sax": "~1.2.4",
+            "stable": "~0.1.6",
+            "unquote": "~1.1.1",
+            "util.promisify": "~1.0.0"
+          }
+        }
+      }
+    },
+    "htmlparser2": {
+      "version": "3.10.0",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.0.tgz",
+      "integrity": "sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==",
+      "dev": true,
+      "requires": {
+        "domelementtype": "^1.3.0",
+        "domhandler": "^2.3.0",
+        "domutils": "^1.5.1",
+        "entities": "^1.1.1",
+        "inherits": "^2.0.1",
+        "readable-stream": "^3.0.6"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz",
+          "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==",
+          "dev": true,
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz",
+          "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        }
+      }
+    },
+    "http-deceiver": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
+      "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=",
+      "dev": true
+    },
+    "http-errors": {
+      "version": "1.6.3",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+      "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+      "dev": true,
+      "requires": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.3",
+        "setprototypeof": "1.1.0",
+        "statuses": ">= 1.4.0 < 2"
+      }
+    },
+    "http-parser-js": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.0.tgz",
+      "integrity": "sha512-cZdEF7r4gfRIq7ezX9J0T+kQmJNOub71dWbgAXVHDct80TKP4MCETtZQ31xyv38UwgzkWPYF/Xc0ge55dW9Z9w==",
+      "dev": true
+    },
+    "http-proxy": {
+      "version": "1.17.0",
+      "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz",
+      "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==",
+      "dev": true,
+      "requires": {
+        "eventemitter3": "^3.0.0",
+        "follow-redirects": "^1.0.0",
+        "requires-port": "^1.0.0"
+      }
+    },
+    "http-proxy-middleware": {
+      "version": "0.19.1",
+      "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz",
+      "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==",
+      "dev": true,
+      "requires": {
+        "http-proxy": "^1.17.0",
+        "is-glob": "^4.0.0",
+        "lodash": "^4.17.11",
+        "micromatch": "^3.1.10"
+      }
+    },
+    "http-signature": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+      "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "jsprim": "^1.2.2",
+        "sshpk": "^1.7.0"
+      }
+    },
+    "https-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+      "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
+      "dev": true
+    },
+    "iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "dev": true,
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      }
+    },
+    "icss-replace-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz",
+      "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=",
+      "dev": true
+    },
+    "icss-utils": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-2.1.0.tgz",
+      "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=",
+      "dev": true,
+      "requires": {
+        "postcss": "^6.0.1"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "postcss": {
+          "version": "6.0.23",
+          "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+          "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.4.1",
+            "source-map": "^0.6.1",
+            "supports-color": "^5.4.0"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "ieee754": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
+      "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==",
+      "dev": true
+    },
+    "iferr": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz",
+      "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
+      "dev": true
+    },
+    "ignore": {
+      "version": "3.3.10",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
+      "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
+      "dev": true
+    },
+    "ignore-loader": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/ignore-loader/-/ignore-loader-0.1.2.tgz",
+      "integrity": "sha1-2B8kA3bQuk8Nd4lyw60lh0EXpGM=",
+      "dev": true
+    },
+    "image-size": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+      "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
+      "dev": true
+    },
+    "immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
+    },
+    "import-fresh": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz",
+      "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==",
+      "dev": true,
+      "requires": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      }
+    },
+    "import-local": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz",
+      "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==",
+      "dev": true,
+      "requires": {
+        "pkg-dir": "^2.0.0",
+        "resolve-cwd": "^2.0.0"
+      }
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+      "dev": true
+    },
+    "in-publish": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz",
+      "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=",
+      "dev": true
+    },
+    "indent-string": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+      "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+      "dev": true,
+      "requires": {
+        "repeating": "^2.0.0"
+      }
+    },
+    "indexes-of": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
+      "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
+      "dev": true
+    },
+    "indexof": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+      "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+    },
+    "ini": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+      "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
+      "dev": true
+    },
+    "inquirer": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz",
+      "integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==",
+      "dev": true,
+      "requires": {
+        "ansi-escapes": "^3.0.0",
+        "chalk": "^2.0.0",
+        "cli-cursor": "^2.1.0",
+        "cli-width": "^2.0.0",
+        "external-editor": "^3.0.0",
+        "figures": "^2.0.0",
+        "lodash": "^4.17.10",
+        "mute-stream": "0.0.7",
+        "run-async": "^2.2.0",
+        "rxjs": "^6.1.0",
+        "string-width": "^2.1.0",
+        "strip-ansi": "^5.0.0",
+        "through": "^2.3.6"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz",
+          "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz",
+          "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^4.0.0"
+          }
+        }
+      }
+    },
+    "internal-ip": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.2.0.tgz",
+      "integrity": "sha512-ZY8Rk+hlvFeuMmG5uH1MXhhdeMntmIaxaInvAmzMq/SHV8rv4Kh+6GiQNNDQd0wZFrcO+FiTBo8lui/osKOyJw==",
+      "dev": true,
+      "requires": {
+        "default-gateway": "^4.0.1",
+        "ipaddr.js": "^1.9.0"
+      },
+      "dependencies": {
+        "ipaddr.js": {
+          "version": "1.9.0",
+          "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
+          "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==",
+          "dev": true
+        }
+      }
+    },
+    "interpret": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz",
+      "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==",
+      "dev": true
+    },
+    "invariant": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+      "dev": true,
+      "requires": {
+        "loose-envify": "^1.0.0"
+      }
+    },
+    "invert-kv": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
+      "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
+      "dev": true
+    },
+    "ip": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
+      "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=",
+      "dev": true
+    },
+    "ip-regex": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
+      "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=",
+      "dev": true
+    },
+    "ipaddr.js": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
+      "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=",
+      "dev": true
+    },
+    "is-absolute-url": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz",
+      "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=",
+      "dev": true
+    },
+    "is-accessor-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+      "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+      "dev": true
+    },
+    "is-binary-path": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+      "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+      "dev": true,
+      "requires": {
+        "binary-extensions": "^1.0.0"
+      }
+    },
+    "is-buffer": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+      "dev": true
+    },
+    "is-builtin-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
+      "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
+      "dev": true,
+      "requires": {
+        "builtin-modules": "^1.0.0"
+      }
+    },
+    "is-callable": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
+      "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
+      "dev": true
+    },
+    "is-data-descriptor": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+      "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-date-object": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
+      "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
+      "dev": true
+    },
+    "is-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "dev": true,
+      "requires": {
+        "is-accessor-descriptor": "^0.1.6",
+        "is-data-descriptor": "^0.1.4",
+        "kind-of": "^5.0.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+          "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+          "dev": true
+        }
+      }
+    },
+    "is-expression": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz",
+      "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=",
+      "dev": true,
+      "requires": {
+        "acorn": "~4.0.2",
+        "object-assign": "^4.0.1"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "4.0.13",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
+          "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=",
+          "dev": true
+        }
+      }
+    },
+    "is-extendable": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+      "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
+      "dev": true
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "dev": true
+    },
+    "is-finite": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+      "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+      "dev": true,
+      "requires": {
+        "number-is-nan": "^1.0.0"
+      }
+    },
+    "is-fullwidth-code-point": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+      "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+      "dev": true
+    },
+    "is-glob": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
+      "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-number": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+      "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-path-cwd": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
+      "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=",
+      "dev": true
+    },
+    "is-path-in-cwd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
+      "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
+      "dev": true,
+      "requires": {
+        "is-path-inside": "^1.0.0"
+      }
+    },
+    "is-path-inside": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
+      "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
+      "dev": true,
+      "requires": {
+        "path-is-inside": "^1.0.1"
+      }
+    },
+    "is-plain-obj": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+      "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
+      "dev": true
+    },
+    "is-plain-object": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+      "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+      "dev": true,
+      "requires": {
+        "isobject": "^3.0.1"
+      }
+    },
+    "is-promise": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
+      "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
+      "dev": true
+    },
+    "is-regex": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
+      "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
+      "dev": true,
+      "requires": {
+        "has": "^1.0.1"
+      }
+    },
+    "is-stream": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
+      "dev": true
+    },
+    "is-svg": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz",
+      "integrity": "sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk=",
+      "dev": true,
+      "requires": {
+        "html-comment-regex": "^1.1.0"
+      }
+    },
+    "is-symbol": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz",
+      "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==",
+      "dev": true,
+      "requires": {
+        "has-symbols": "^1.0.0"
+      }
+    },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+      "dev": true
+    },
+    "is-utf8": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+      "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
+      "dev": true
+    },
+    "is-windows": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+      "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+      "dev": true
+    },
+    "is-wsl": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+      "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
+      "dev": true
+    },
+    "isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+    },
+    "isbinaryfile": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz",
+      "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==",
+      "dev": true,
+      "requires": {
+        "buffer-alloc": "^1.2.0"
+      }
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "dev": true
+    },
+    "isobject": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+      "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+      "dev": true
+    },
+    "isstream": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
+      "dev": true
+    },
+    "jquery": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz",
+      "integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c="
+    },
+    "js-base64": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.0.tgz",
+      "integrity": "sha512-wlEBIZ5LP8usDylWbDNhKPEFVFdI5hCHpnVoT/Ysvoi/PRhJENm/Rlh9TvjYB38HFfKZN7OzEbRjmjvLkFw11g==",
+      "dev": true
+    },
+    "js-levenshtein": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
+      "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
+      "dev": true
+    },
+    "js-stringify": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
+      "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=",
+      "dev": true
+    },
+    "js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+    },
+    "js-yaml": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz",
+      "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=",
+      "dev": true,
+      "requires": {
+        "argparse": "^1.0.7",
+        "esprima": "^2.6.0"
+      }
+    },
+    "jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+      "dev": true
+    },
+    "jsesc": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
+    },
+    "json-bigint": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz",
+      "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=",
+      "requires": {
+        "bignumber.js": "^7.0.0"
+      }
+    },
+    "json-parse-better-errors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+      "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+      "dev": true
+    },
+    "json-schema": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+      "dev": true
+    },
+    "json-schema-traverse": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
+      "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
+      "dev": true
+    },
+    "json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+      "dev": true
+    },
+    "json-stringify-safe": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+      "dev": true
+    },
+    "json3": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
+      "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=",
+      "dev": true
+    },
+    "json5": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+      "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
+      "dev": true
+    },
+    "jsondiffpatch": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.2.5.tgz",
+      "integrity": "sha1-UDYdmVz4yGE36NVYnyD6UiDbNRE=",
+      "requires": {
+        "chalk": "^0.5.1"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz",
+          "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94="
+        },
+        "chalk": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz",
+          "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=",
+          "requires": {
+            "ansi-styles": "^1.1.0",
+            "escape-string-regexp": "^1.0.0",
+            "has-ansi": "^0.1.0",
+            "strip-ansi": "^0.3.0",
+            "supports-color": "^0.2.0"
+          }
+        },
+        "supports-color": {
+          "version": "0.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz",
+          "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo="
+        }
+      }
+    },
+    "jsprim": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+      "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "1.0.0",
+        "extsprintf": "1.3.0",
+        "json-schema": "0.2.3",
+        "verror": "1.10.0"
+      }
+    },
+    "jstransformer": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz",
+      "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=",
+      "dev": true,
+      "requires": {
+        "is-promise": "^2.0.0",
+        "promise": "^7.0.1"
+      }
+    },
+    "jszip": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz",
+      "integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==",
+      "requires": {
+        "core-js": "~2.3.0",
+        "es6-promise": "~3.0.2",
+        "lie": "~3.1.0",
+        "pako": "~1.0.2",
+        "readable-stream": "~2.0.6"
+      },
+      "dependencies": {
+        "core-js": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz",
+          "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU="
+        }
+      }
+    },
+    "karma": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/karma/-/karma-4.0.0.tgz",
+      "integrity": "sha512-EFoFs3F6G0BcUGPNOn/YloGOb3h09hzTguyXlg6loHlKY76qbJikkcyPk43m2kfRF65TUGda/mig29QQtyhm1g==",
+      "dev": true,
+      "requires": {
+        "bluebird": "^3.3.0",
+        "body-parser": "^1.16.1",
+        "chokidar": "^2.0.3",
+        "colors": "^1.1.0",
+        "combine-lists": "^1.0.0",
+        "connect": "^3.6.0",
+        "core-js": "^2.2.0",
+        "di": "^0.0.1",
+        "dom-serialize": "^2.2.0",
+        "expand-braces": "^0.1.1",
+        "flatted": "^2.0.0",
+        "glob": "^7.1.1",
+        "graceful-fs": "^4.1.2",
+        "http-proxy": "^1.13.0",
+        "isbinaryfile": "^3.0.0",
+        "lodash": "^4.17.5",
+        "log4js": "^3.0.0",
+        "mime": "^2.3.1",
+        "minimatch": "^3.0.2",
+        "optimist": "^0.6.1",
+        "qjobs": "^1.1.4",
+        "range-parser": "^1.2.0",
+        "rimraf": "^2.6.0",
+        "safe-buffer": "^5.0.1",
+        "socket.io": "2.1.1",
+        "source-map": "^0.6.1",
+        "tmp": "0.0.33",
+        "useragent": "2.3.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "karma-babel-preprocessor": {
+      "version": "8.0.0-beta.0",
+      "resolved": "https://registry.npmjs.org/karma-babel-preprocessor/-/karma-babel-preprocessor-8.0.0-beta.0.tgz",
+      "integrity": "sha512-nv3GbDAKdonWuTJc+Kg4jEdRXzoP7uKKQ6HfTJb5PNTY+OJYKzrtUBUSez/wrutUFtztVT+MQxJHamd7MNCmBQ==",
+      "dev": true
+    },
+    "karma-chrome-launcher": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz",
+      "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==",
+      "dev": true,
+      "requires": {
+        "fs-access": "^1.0.0",
+        "which": "^1.2.1"
+      }
+    },
+    "karma-mocha": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/karma-mocha/-/karma-mocha-1.3.0.tgz",
+      "integrity": "sha1-7qrH/8DiAetjxGdEDStpx883eL8=",
+      "dev": true,
+      "requires": {
+        "minimist": "1.2.0"
+      }
+    },
+    "karma-mocha-reporter": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.3.tgz",
+      "integrity": "sha1-BP3aRaHZaXpzhxx0ciI8WBcBqyA=",
+      "dev": true,
+      "requires": {
+        "chalk": "1.1.3"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "has-ansi": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+          "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
+    "karma-teamcity-reporter": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/karma-teamcity-reporter/-/karma-teamcity-reporter-1.0.0.tgz",
+      "integrity": "sha1-0zwSF3U8FBiX9kVg8l8PN76QUjM=",
+      "dev": true
+    },
+    "karma-webpack": {
+      "version": "4.0.0-rc.2",
+      "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-4.0.0-rc.2.tgz",
+      "integrity": "sha512-Wuiq/xFBsbJMsHhYy5SYXxSp7Q0b8uzAG8+Siuo56ntoi5GluPE5LK3Mzl2UtD4k1leFwL6IeIE6Q+tk4F6k9Q==",
+      "dev": true,
+      "requires": {
+        "async": "^2.0.0",
+        "loader-utils": "^1.1.0",
+        "lodash": "^4.17.10",
+        "source-map": "^0.5.6",
+        "webpack-dev-middleware": "^3.2.0"
+      }
+    },
+    "killable": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
+      "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==",
+      "dev": true
+    },
+    "kind-of": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+      "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+      "dev": true
+    },
+    "lazy-cache": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
+      "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=",
+      "dev": true
+    },
+    "lcid": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
+      "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
+      "dev": true,
+      "requires": {
+        "invert-kv": "^1.0.0"
+      }
+    },
+    "levn": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2"
+      }
+    },
+    "lie": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
+      "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
+      "requires": {
+        "immediate": "~3.0.5"
+      }
+    },
+    "load-json-file": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+      "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "parse-json": "^2.2.0",
+        "pify": "^2.0.0",
+        "pinkie-promise": "^2.0.0",
+        "strip-bom": "^2.0.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+          "dev": true
+        }
+      }
+    },
+    "loader-fs-cache": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.1.tgz",
+      "integrity": "sha1-VuC/CL2XCLJqdltoUJhAyN7J/bw=",
+      "dev": true,
+      "requires": {
+        "find-cache-dir": "^0.1.1",
+        "mkdirp": "0.5.1"
+      },
+      "dependencies": {
+        "find-cache-dir": {
+          "version": "0.1.1",
+          "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz",
+          "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=",
+          "dev": true,
+          "requires": {
+            "commondir": "^1.0.1",
+            "mkdirp": "^0.5.1",
+            "pkg-dir": "^1.0.0"
+          }
+        },
+        "find-up": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+          "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+          "dev": true,
+          "requires": {
+            "path-exists": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          }
+        },
+        "path-exists": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+          "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+          "dev": true,
+          "requires": {
+            "pinkie-promise": "^2.0.0"
+          }
+        },
+        "pkg-dir": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz",
+          "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=",
+          "dev": true,
+          "requires": {
+            "find-up": "^1.0.0"
+          }
+        }
+      }
+    },
+    "loader-runner": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.1.tgz",
+      "integrity": "sha512-By6ZFY7ETWOc9RFaAIb23IjJVcM4dvJC/N57nmdz9RSkMXvAXGI7SyVlAw3v8vjtDRlqThgVDVmTnr9fqMlxkw==",
+      "dev": true
+    },
+    "loader-utils": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
+      "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
+      "dev": true,
+      "requires": {
+        "big.js": "^5.2.2",
+        "emojis-list": "^2.0.0",
+        "json5": "^1.0.1"
+      },
+      "dependencies": {
+        "json5": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+          "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+          "dev": true,
+          "requires": {
+            "minimist": "^1.2.0"
+          }
+        }
+      }
+    },
+    "locate-path": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+      "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+      "dev": true,
+      "requires": {
+        "p-locate": "^2.0.0",
+        "path-exists": "^3.0.0"
+      }
+    },
+    "lodash": {
+      "version": "4.17.11",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
+    },
+    "lodash._baseassign": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz",
+      "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=",
+      "dev": true,
+      "requires": {
+        "lodash._basecopy": "^3.0.0",
+        "lodash.keys": "^3.0.0"
+      }
+    },
+    "lodash._basecopy": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
+      "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=",
+      "dev": true
+    },
+    "lodash._bindcallback": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz",
+      "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=",
+      "dev": true
+    },
+    "lodash._createassigner": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz",
+      "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=",
+      "dev": true,
+      "requires": {
+        "lodash._bindcallback": "^3.0.0",
+        "lodash._isiterateecall": "^3.0.0",
+        "lodash.restparam": "^3.0.0"
+      }
+    },
+    "lodash._getnative": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
+      "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=",
+      "dev": true
+    },
+    "lodash._isiterateecall": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz",
+      "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=",
+      "dev": true
+    },
+    "lodash.assign": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
+      "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
+      "dev": true
+    },
+    "lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=",
+      "dev": true
+    },
+    "lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
+      "dev": true
+    },
+    "lodash.debounce": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+      "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
+      "dev": true
+    },
+    "lodash.defaults": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+      "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=",
+      "dev": true
+    },
+    "lodash.isarguments": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
+      "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=",
+      "dev": true
+    },
+    "lodash.isarray": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
+      "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
+      "dev": true
+    },
+    "lodash.keys": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
+      "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
+      "dev": true,
+      "requires": {
+        "lodash._getnative": "^3.0.0",
+        "lodash.isarguments": "^3.0.0",
+        "lodash.isarray": "^3.0.0"
+      }
+    },
+    "lodash.memoize": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+      "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
+      "dev": true
+    },
+    "lodash.mergewith": {
+      "version": "4.6.1",
+      "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
+      "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==",
+      "dev": true
+    },
+    "lodash.restparam": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
+      "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=",
+      "dev": true
+    },
+    "lodash.tail": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz",
+      "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=",
+      "dev": true
+    },
+    "lodash.unescape": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz",
+      "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=",
+      "dev": true
+    },
+    "lodash.uniq": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+      "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
+      "dev": true
+    },
+    "log-symbols": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
+      "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.0.1"
+      }
+    },
+    "log4js": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/log4js/-/log4js-3.0.6.tgz",
+      "integrity": "sha512-ezXZk6oPJCWL483zj64pNkMuY/NcRX5MPiB0zE6tjZM137aeusrOnW1ecxgF9cmwMWkBMhjteQxBPoZBh9FDxQ==",
+      "dev": true,
+      "requires": {
+        "circular-json": "^0.5.5",
+        "date-format": "^1.2.0",
+        "debug": "^3.1.0",
+        "rfdc": "^1.1.2",
+        "streamroller": "0.7.0"
+      },
+      "dependencies": {
+        "circular-json": {
+          "version": "0.5.9",
+          "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.9.tgz",
+          "integrity": "sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ==",
+          "dev": true
+        },
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
+      }
+    },
+    "loglevel": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz",
+      "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=",
+      "dev": true
+    },
+    "lolex": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz",
+      "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=",
+      "dev": true
+    },
+    "longest": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
+      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
+      "dev": true
+    },
+    "loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "dev": true,
+      "requires": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      }
+    },
+    "loud-rejection": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+      "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+      "dev": true,
+      "requires": {
+        "currently-unhandled": "^0.4.1",
+        "signal-exit": "^3.0.0"
+      }
+    },
+    "lower-case": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
+      "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=",
+      "dev": true
+    },
+    "lru-cache": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
+      "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+      "dev": true,
+      "requires": {
+        "pseudomap": "^1.0.2",
+        "yallist": "^2.1.2"
+      }
+    },
+    "make-dir": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
+      "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
+      "dev": true,
+      "requires": {
+        "pify": "^3.0.0"
+      }
+    },
+    "mamacro": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz",
+      "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==",
+      "dev": true
+    },
+    "map-age-cleaner": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
+      "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
+      "dev": true,
+      "requires": {
+        "p-defer": "^1.0.0"
+      }
+    },
+    "map-cache": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+      "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
+      "dev": true
+    },
+    "map-obj": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+      "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
+      "dev": true
+    },
+    "map-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+      "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+      "dev": true,
+      "requires": {
+        "object-visit": "^1.0.0"
+      }
+    },
+    "math-expression-evaluator": {
+      "version": "1.2.17",
+      "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz",
+      "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=",
+      "dev": true
+    },
+    "md5.js": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
+      "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+      "dev": true,
+      "requires": {
+        "hash-base": "^3.0.0",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.1.2"
+      }
+    },
+    "mdn-data": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz",
+      "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==",
+      "dev": true
+    },
+    "media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
+      "dev": true
+    },
+    "mem": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/mem/-/mem-4.0.0.tgz",
+      "integrity": "sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA==",
+      "dev": true,
+      "requires": {
+        "map-age-cleaner": "^0.1.1",
+        "mimic-fn": "^1.0.0",
+        "p-is-promise": "^1.1.0"
+      }
+    },
+    "memory-fs": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
+      "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
+      "dev": true,
+      "requires": {
+        "errno": "^0.1.3",
+        "readable-stream": "^2.0.1"
+      }
+    },
+    "meow": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+      "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+      "dev": true,
+      "requires": {
+        "camelcase-keys": "^2.0.0",
+        "decamelize": "^1.1.2",
+        "loud-rejection": "^1.0.0",
+        "map-obj": "^1.0.1",
+        "minimist": "^1.1.3",
+        "normalize-package-data": "^2.3.4",
+        "object-assign": "^4.0.1",
+        "read-pkg-up": "^1.0.1",
+        "redent": "^1.0.0",
+        "trim-newlines": "^1.0.0"
+      }
+    },
+    "merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
+      "dev": true
+    },
+    "merge-options": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-1.0.1.tgz",
+      "integrity": "sha512-iuPV41VWKWBIOpBsjoxjDZw8/GbSfZ2mk7N1453bwMrfzdrIk7EzBd+8UVR6rkw67th7xnk9Dytl3J+lHPdxvg==",
+      "dev": true,
+      "requires": {
+        "is-plain-obj": "^1.1"
+      }
+    },
+    "merge2": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.3.tgz",
+      "integrity": "sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==",
+      "dev": true
+    },
+    "methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
+      "dev": true
+    },
+    "micromatch": {
+      "version": "3.1.10",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+      "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+      "dev": true,
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "braces": "^2.3.1",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "extglob": "^2.0.4",
+        "fragment-cache": "^0.2.1",
+        "kind-of": "^6.0.2",
+        "nanomatch": "^1.2.9",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.2"
+      }
+    },
+    "miller-rabin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+      "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.0.0",
+        "brorand": "^1.0.1"
+      }
+    },
+    "mime": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz",
+      "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==",
+      "dev": true
+    },
+    "mime-db": {
+      "version": "1.37.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
+      "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==",
+      "dev": true
+    },
+    "mime-types": {
+      "version": "2.1.21",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
+      "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
+      "dev": true,
+      "requires": {
+        "mime-db": "~1.37.0"
+      }
+    },
+    "mimic-fn": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+      "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+      "dev": true
+    },
+    "mini-css-extract-plugin": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.2.tgz",
+      "integrity": "sha512-ots7URQH4wccfJq9Ssrzu2+qupbncAce4TmTzunI9CIwlQMp2XI+WNUw6xWF6MMAGAm1cbUVINrSjATaVMyKXg==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "schema-utils": "^1.0.0",
+        "webpack-sources": "^1.1.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.6.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz",
+          "integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^2.0.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "fast-deep-equal": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+          "dev": true
+        },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        },
+        "schema-utils": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+          "dev": true,
+          "requires": {
+            "ajv": "^6.1.0",
+            "ajv-errors": "^1.0.0",
+            "ajv-keywords": "^3.1.0"
+          }
+        }
+      }
+    },
+    "minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "dev": true
+    },
+    "minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
+      "dev": true
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "minimist": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+      "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+      "dev": true
+    },
+    "mississippi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz",
+      "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==",
+      "dev": true,
+      "requires": {
+        "concat-stream": "^1.5.0",
+        "duplexify": "^3.4.2",
+        "end-of-stream": "^1.1.0",
+        "flush-write-stream": "^1.0.0",
+        "from2": "^2.1.0",
+        "parallel-transform": "^1.1.0",
+        "pump": "^2.0.1",
+        "pumpify": "^1.3.3",
+        "stream-each": "^1.1.0",
+        "through2": "^2.0.0"
+      }
+    },
+    "mitt": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.1.2.tgz",
+      "integrity": "sha1-OA5hSA1qYVtmDwertg1R4KTkvtY=",
+      "dev": true
+    },
+    "mixin-deep": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
+      "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+      "dev": true,
+      "requires": {
+        "for-in": "^1.0.2",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "dev": true,
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "mixin-object": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz",
+      "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=",
+      "dev": true,
+      "requires": {
+        "for-in": "^0.1.3",
+        "is-extendable": "^0.1.1"
+      },
+      "dependencies": {
+        "for-in": {
+          "version": "0.1.8",
+          "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz",
+          "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=",
+          "dev": true
+        }
+      }
+    },
+    "mkdirp": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "dev": true,
+      "requires": {
+        "minimist": "0.0.8"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "0.0.8",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+          "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+          "dev": true
+        }
+      }
+    },
+    "mocha": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.0.1.tgz",
+      "integrity": "sha512-tQzCxWqxSD6Oyg5r7Ptbev0yAMD8p+Vfh4snPFuiUsWqYj0eVYTDT2DkEY307FTj0WRlIWN9rWMMAUzRmijgVQ==",
+      "dev": true,
+      "requires": {
+        "ansi-colors": "3.2.3",
+        "browser-stdout": "1.3.1",
+        "debug": "3.2.6",
+        "diff": "3.5.0",
+        "escape-string-regexp": "1.0.5",
+        "findup-sync": "2.0.0",
+        "glob": "7.1.3",
+        "growl": "1.10.5",
+        "he": "1.2.0",
+        "js-yaml": "3.12.0",
+        "log-symbols": "2.2.0",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.1",
+        "ms": "2.1.1",
+        "node-environment-flags": "1.0.4",
+        "object.assign": "4.1.0",
+        "strip-json-comments": "2.0.1",
+        "supports-color": "6.0.0",
+        "which": "1.3.1",
+        "wide-align": "1.1.3",
+        "yargs": "12.0.5",
+        "yargs-parser": "11.1.1",
+        "yargs-unparser": "1.5.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "camelcase": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz",
+          "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==",
+          "dev": true
+        },
+        "cliui": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
+          "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
+          "dev": true,
+          "requires": {
+            "string-width": "^2.1.1",
+            "strip-ansi": "^4.0.0",
+            "wrap-ansi": "^2.0.0"
+          }
+        },
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "diff": {
+          "version": "3.5.0",
+          "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+          "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+          "dev": true
+        },
+        "esprima": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+          "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+          "dev": true
+        },
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "glob": {
+          "version": "7.1.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+          "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "invert-kv": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
+          "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
+          "dev": true
+        },
+        "js-yaml": {
+          "version": "3.12.0",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
+          "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
+          "dev": true,
+          "requires": {
+            "argparse": "^1.0.7",
+            "esprima": "^4.0.0"
+          }
+        },
+        "lcid": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
+          "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
+          "dev": true,
+          "requires": {
+            "invert-kv": "^2.0.0"
+          }
+        },
+        "locate-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^3.0.0",
+            "path-exists": "^3.0.0"
+          }
+        },
+        "os-locale": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
+          "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
+          "dev": true,
+          "requires": {
+            "execa": "^1.0.0",
+            "lcid": "^2.0.0",
+            "mem": "^4.0.0"
+          }
+        },
+        "p-limit": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz",
+          "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==",
+          "dev": true,
+          "requires": {
+            "p-try": "^2.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.0.0"
+          }
+        },
+        "p-try": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz",
+          "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
+          "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        },
+        "which-module": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+          "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+          "dev": true
+        },
+        "yargs": {
+          "version": "12.0.5",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
+          "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
+          "dev": true,
+          "requires": {
+            "cliui": "^4.0.0",
+            "decamelize": "^1.2.0",
+            "find-up": "^3.0.0",
+            "get-caller-file": "^1.0.1",
+            "os-locale": "^3.0.0",
+            "require-directory": "^2.1.1",
+            "require-main-filename": "^1.0.1",
+            "set-blocking": "^2.0.0",
+            "string-width": "^2.0.0",
+            "which-module": "^2.0.0",
+            "y18n": "^3.2.1 || ^4.0.0",
+            "yargs-parser": "^11.1.1"
+          }
+        },
+        "yargs-parser": {
+          "version": "11.1.1",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
+          "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
+          "dev": true,
+          "requires": {
+            "camelcase": "^5.0.0",
+            "decamelize": "^1.2.0"
+          }
+        }
+      }
+    },
+    "mocha-teamcity-reporter": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/mocha-teamcity-reporter/-/mocha-teamcity-reporter-1.1.1.tgz",
+      "integrity": "sha1-aW5ns9PTv3Iiw2CPNSNTf4VBFDk=",
+      "dev": true,
+      "requires": {
+        "mocha": ">=1.13.0"
+      }
+    },
+    "moment": {
+      "version": "2.23.0",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.23.0.tgz",
+      "integrity": "sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA=="
+    },
+    "move-concurrently": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
+      "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=",
+      "dev": true,
+      "requires": {
+        "aproba": "^1.1.1",
+        "copy-concurrently": "^1.0.0",
+        "fs-write-stream-atomic": "^1.0.8",
+        "mkdirp": "^0.5.1",
+        "rimraf": "^2.5.4",
+        "run-queue": "^1.0.3"
+      }
+    },
+    "ms": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+      "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+    },
+    "multicast-dns": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz",
+      "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==",
+      "dev": true,
+      "requires": {
+        "dns-packet": "^1.3.1",
+        "thunky": "^1.0.2"
+      }
+    },
+    "multicast-dns-service-types": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
+      "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
+      "dev": true
+    },
+    "mute-stream": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+      "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
+      "dev": true
+    },
+    "nan": {
+      "version": "2.12.1",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz",
+      "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==",
+      "dev": true
+    },
+    "nanomatch": {
+      "version": "1.2.13",
+      "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+      "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+      "dev": true,
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "fragment-cache": "^0.2.1",
+        "is-windows": "^1.0.2",
+        "kind-of": "^6.0.2",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      }
+    },
+    "native-promise-only": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz",
+      "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=",
+      "dev": true
+    },
+    "natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+      "dev": true
+    },
+    "natural-compare-lite": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
+      "integrity": "sha1-F7CVgZiJef3a/gIB6TG6kzyWy7Q="
+    },
+    "negotiator": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
+      "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=",
+      "dev": true
+    },
+    "neo-async": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz",
+      "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==",
+      "dev": true
+    },
+    "nice-try": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+      "dev": true
+    },
+    "no-case": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
+      "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
+      "dev": true,
+      "requires": {
+        "lower-case": "^1.1.1"
+      }
+    },
+    "node-environment-flags": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.4.tgz",
+      "integrity": "sha512-M9rwCnWVLW7PX+NUWe3ejEdiLYinRpsEre9hMkU/6NS4h+EEulYaDH1gCEZ2gyXsmw+RXYDaV2JkkTNcsPDJ0Q==",
+      "dev": true,
+      "requires": {
+        "object.getownpropertydescriptors": "^2.0.3"
+      }
+    },
+    "node-fetch": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
+      "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
+      "dev": true,
+      "requires": {
+        "encoding": "^0.1.11",
+        "is-stream": "^1.0.1"
+      }
+    },
+    "node-forge": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz",
+      "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==",
+      "dev": true
+    },
+    "node-gyp": {
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
+      "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==",
+      "dev": true,
+      "requires": {
+        "fstream": "^1.0.0",
+        "glob": "^7.0.3",
+        "graceful-fs": "^4.1.2",
+        "mkdirp": "^0.5.0",
+        "nopt": "2 || 3",
+        "npmlog": "0 || 1 || 2 || 3 || 4",
+        "osenv": "0",
+        "request": "^2.87.0",
+        "rimraf": "2",
+        "semver": "~5.3.0",
+        "tar": "^2.0.0",
+        "which": "1"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+          "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
+          "dev": true
+        }
+      }
+    },
+    "node-libs-browser": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz",
+      "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==",
+      "dev": true,
+      "requires": {
+        "assert": "^1.1.1",
+        "browserify-zlib": "^0.2.0",
+        "buffer": "^4.3.0",
+        "console-browserify": "^1.1.0",
+        "constants-browserify": "^1.0.0",
+        "crypto-browserify": "^3.11.0",
+        "domain-browser": "^1.1.1",
+        "events": "^1.0.0",
+        "https-browserify": "^1.0.0",
+        "os-browserify": "^0.3.0",
+        "path-browserify": "0.0.0",
+        "process": "^0.11.10",
+        "punycode": "^1.2.4",
+        "querystring-es3": "^0.2.0",
+        "readable-stream": "^2.3.3",
+        "stream-browserify": "^2.0.1",
+        "stream-http": "^2.7.2",
+        "string_decoder": "^1.0.0",
+        "timers-browserify": "^2.0.4",
+        "tty-browserify": "0.0.0",
+        "url": "^0.11.0",
+        "util": "^0.10.3",
+        "vm-browserify": "0.0.4"
+      },
+      "dependencies": {
+        "buffer": {
+          "version": "4.9.1",
+          "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
+          "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
+          "dev": true,
+          "requires": {
+            "base64-js": "^1.0.2",
+            "ieee754": "^1.1.4",
+            "isarray": "^1.0.0"
+          }
+        },
+        "path-browserify": {
+          "version": "0.0.0",
+          "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz",
+          "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=",
+          "dev": true
+        },
+        "process-nextick-args": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+          "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+          "dev": true
+        },
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          },
+          "dependencies": {
+            "string_decoder": {
+              "version": "1.1.1",
+              "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+              "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+              "dev": true,
+              "requires": {
+                "safe-buffer": "~5.1.0"
+              }
+            }
+          }
+        },
+        "string_decoder": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz",
+          "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        },
+        "timers-browserify": {
+          "version": "2.0.10",
+          "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz",
+          "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==",
+          "dev": true,
+          "requires": {
+            "setimmediate": "^1.0.4"
+          }
+        },
+        "tty-browserify": {
+          "version": "0.0.0",
+          "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+          "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
+          "dev": true
+        }
+      }
+    },
+    "node-releases": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.3.tgz",
+      "integrity": "sha512-6VrvH7z6jqqNFY200kdB6HdzkgM96Oaj9v3dqGfgp6mF+cHmU4wyQKZ2/WPDRVoR0Jz9KqbamaBN0ZhdUaysUQ==",
+      "dev": true,
+      "requires": {
+        "semver": "^5.3.0"
+      }
+    },
+    "node-sass": {
+      "version": "4.10.0",
+      "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.10.0.tgz",
+      "integrity": "sha512-fDQJfXszw6vek63Fe/ldkYXmRYK/QS6NbvM3i5oEo9ntPDy4XX7BcKZyTKv+/kSSxRtXXc7l+MSwEmYc0CSy6Q==",
+      "dev": true,
+      "requires": {
+        "async-foreach": "^0.1.3",
+        "chalk": "^1.1.1",
+        "cross-spawn": "^3.0.0",
+        "gaze": "^1.0.0",
+        "get-stdin": "^4.0.1",
+        "glob": "^7.0.3",
+        "in-publish": "^2.0.0",
+        "lodash.assign": "^4.2.0",
+        "lodash.clonedeep": "^4.3.2",
+        "lodash.mergewith": "^4.6.0",
+        "meow": "^3.7.0",
+        "mkdirp": "^0.5.1",
+        "nan": "^2.10.0",
+        "node-gyp": "^3.8.0",
+        "npmlog": "^4.0.0",
+        "request": "^2.88.0",
+        "sass-graph": "^2.2.4",
+        "stdout-stream": "^1.4.0",
+        "true-case-path": "^1.0.2"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "cross-spawn": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
+          "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^4.0.1",
+            "which": "^1.2.9"
+          }
+        },
+        "has-ansi": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+          "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
+    "nopt": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+      "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+      "dev": true,
+      "requires": {
+        "abbrev": "1"
+      }
+    },
+    "normalize-package-data": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
+      "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
+      "dev": true,
+      "requires": {
+        "hosted-git-info": "^2.1.4",
+        "is-builtin-module": "^1.0.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      }
+    },
+    "normalize-path": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+      "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+      "dev": true,
+      "requires": {
+        "remove-trailing-separator": "^1.0.1"
+      }
+    },
+    "normalize-range": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+      "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
+      "dev": true
+    },
+    "normalize-url": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz",
+      "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=",
+      "dev": true,
+      "requires": {
+        "object-assign": "^4.0.1",
+        "prepend-http": "^1.0.0",
+        "query-string": "^4.1.0",
+        "sort-keys": "^1.0.0"
+      }
+    },
+    "npm-run-path": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+      "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+      "dev": true,
+      "requires": {
+        "path-key": "^2.0.0"
+      }
+    },
+    "npmlog": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+      "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+      "dev": true,
+      "requires": {
+        "are-we-there-yet": "~1.1.2",
+        "console-control-strings": "~1.1.0",
+        "gauge": "~2.7.3",
+        "set-blocking": "~2.0.0"
+      }
+    },
+    "nth-check": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
+      "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
+      "dev": true,
+      "requires": {
+        "boolbase": "~1.0.0"
+      }
+    },
+    "null-check": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz",
+      "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=",
+      "dev": true
+    },
+    "num2fraction": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
+      "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=",
+      "dev": true
+    },
+    "number-is-nan": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+      "dev": true
+    },
+    "nvd3": {
+      "version": "1.8.6",
+      "resolved": "https://registry.npmjs.org/nvd3/-/nvd3-1.8.6.tgz",
+      "integrity": "sha1-LT66dL8zNjtRAevx0JPFmlOuc8Q="
+    },
+    "oauth-sign": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
+      "dev": true
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+      "dev": true
+    },
+    "object-component": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+      "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
+    },
+    "object-copy": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+      "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+      "dev": true,
+      "requires": {
+        "copy-descriptor": "^0.1.0",
+        "define-property": "^0.2.5",
+        "kind-of": "^3.0.3"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "object-hash": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz",
+      "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==",
+      "dev": true
+    },
+    "object-keys": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz",
+      "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==",
+      "dev": true
+    },
+    "object-path": {
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.9.2.tgz",
+      "integrity": "sha1-D9mnT8X60a45aLWGvaXGMr1sBaU=",
+      "dev": true
+    },
+    "object-visit": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+      "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+      "dev": true,
+      "requires": {
+        "isobject": "^3.0.0"
+      }
+    },
+    "object.assign": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+      "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "function-bind": "^1.1.1",
+        "has-symbols": "^1.0.0",
+        "object-keys": "^1.0.11"
+      }
+    },
+    "object.getownpropertydescriptors": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz",
+      "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "es-abstract": "^1.5.1"
+      }
+    },
+    "object.pick": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+      "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+      "dev": true,
+      "requires": {
+        "isobject": "^3.0.1"
+      }
+    },
+    "object.values": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz",
+      "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.12.0",
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3"
+      }
+    },
+    "obuf": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
+      "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
+      "dev": true
+    },
+    "on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+      "dev": true,
+      "requires": {
+        "ee-first": "1.1.1"
+      }
+    },
+    "on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+      "dev": true
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "onetime": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+      "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+      "dev": true,
+      "requires": {
+        "mimic-fn": "^1.0.0"
+      }
+    },
+    "opn": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/opn/-/opn-5.4.0.tgz",
+      "integrity": "sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw==",
+      "dev": true,
+      "requires": {
+        "is-wsl": "^1.1.0"
+      }
+    },
+    "optimist": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+      "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+      "dev": true,
+      "requires": {
+        "minimist": "~0.0.1",
+        "wordwrap": "~0.0.2"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "0.0.10",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
+          "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=",
+          "dev": true
+        },
+        "wordwrap": {
+          "version": "0.0.3",
+          "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
+          "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
+          "dev": true
+        }
+      }
+    },
+    "optionator": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
+      "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
+      "dev": true,
+      "requires": {
+        "deep-is": "~0.1.3",
+        "fast-levenshtein": "~2.0.4",
+        "levn": "~0.3.0",
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2",
+        "wordwrap": "~1.0.0"
+      }
+    },
+    "original": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
+      "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==",
+      "dev": true,
+      "requires": {
+        "url-parse": "^1.4.3"
+      }
+    },
+    "os-browserify": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+      "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
+      "dev": true
+    },
+    "os-homedir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+      "dev": true
+    },
+    "os-locale": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+      "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
+      "dev": true,
+      "requires": {
+        "lcid": "^1.0.0"
+      }
+    },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+      "dev": true
+    },
+    "osenv": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+      "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+      "dev": true,
+      "requires": {
+        "os-homedir": "^1.0.0",
+        "os-tmpdir": "^1.0.0"
+      }
+    },
+    "outdent": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz",
+      "integrity": "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="
+    },
+    "p-defer": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
+      "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=",
+      "dev": true
+    },
+    "p-finally": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+      "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
+      "dev": true
+    },
+    "p-is-promise": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz",
+      "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=",
+      "dev": true
+    },
+    "p-limit": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+      "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+      "dev": true,
+      "requires": {
+        "p-try": "^1.0.0"
+      }
+    },
+    "p-locate": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+      "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+      "dev": true,
+      "requires": {
+        "p-limit": "^1.1.0"
+      }
+    },
+    "p-map": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz",
+      "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==",
+      "dev": true
+    },
+    "p-try": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+      "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
+      "dev": true
+    },
+    "pako": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
+      "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg=="
+    },
+    "parallel-transform": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz",
+      "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=",
+      "dev": true,
+      "requires": {
+        "cyclist": "~0.2.2",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.1.5"
+      },
+      "dependencies": {
+        "process-nextick-args": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+          "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        }
+      }
+    },
+    "param-case": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
+      "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
+      "dev": true,
+      "requires": {
+        "no-case": "^2.2.0"
+      }
+    },
+    "parent-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.0.tgz",
+      "integrity": "sha512-8Mf5juOMmiE4FcmzYc4IaiS9L3+9paz2KOiXzkRviCP6aDmN49Hz6EMWz0lGNp9pX80GvvAuLADtyGfW/Em3TA==",
+      "dev": true,
+      "requires": {
+        "callsites": "^3.0.0"
+      }
+    },
+    "parse-asn1": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
+      "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
+      "dev": true,
+      "requires": {
+        "asn1.js": "^4.0.0",
+        "browserify-aes": "^1.0.0",
+        "create-hash": "^1.1.0",
+        "evp_bytestokey": "^1.0.0",
+        "pbkdf2": "^3.0.3"
+      }
+    },
+    "parse-json": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+      "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+      "dev": true,
+      "requires": {
+        "error-ex": "^1.2.0"
+      }
+    },
+    "parse-passwd": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+      "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
+      "dev": true
+    },
+    "parseqs": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+      "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseuri": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+      "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseurl": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
+      "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=",
+      "dev": true
+    },
+    "pascalcase": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+      "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
+      "dev": true
+    },
+    "path-dirname": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+      "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
+      "dev": true
+    },
+    "path-exists": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+      "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+      "dev": true
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+      "dev": true
+    },
+    "path-is-inside": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+      "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+      "dev": true
+    },
+    "path-key": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+      "dev": true
+    },
+    "path-parse": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+      "dev": true
+    },
+    "path-to-regexp": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
+      "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
+      "dev": true,
+      "requires": {
+        "isarray": "0.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+          "dev": true
+        }
+      }
+    },
+    "path-type": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+      "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+      "dev": true,
+      "requires": {
+        "pify": "^3.0.0"
+      }
+    },
+    "pathval": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
+      "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
+      "dev": true
+    },
+    "pbkdf2": {
+      "version": "3.0.17",
+      "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
+      "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
+      "dev": true,
+      "requires": {
+        "create-hash": "^1.1.2",
+        "create-hmac": "^1.1.4",
+        "ripemd160": "^2.0.1",
+        "safe-buffer": "^5.0.1",
+        "sha.js": "^2.4.8"
+      }
+    },
+    "performance-now": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
+      "dev": true
+    },
+    "pify": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+      "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+      "dev": true
+    },
+    "pinkie": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+      "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+      "dev": true
+    },
+    "pinkie-promise": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+      "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+      "dev": true,
+      "requires": {
+        "pinkie": "^2.0.0"
+      }
+    },
+    "pkg-dir": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
+      "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=",
+      "dev": true,
+      "requires": {
+        "find-up": "^2.1.0"
+      }
+    },
+    "pluralize": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz",
+      "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==",
+      "dev": true
+    },
+    "portfinder": {
+      "version": "1.0.20",
+      "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.20.tgz",
+      "integrity": "sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw==",
+      "dev": true,
+      "requires": {
+        "async": "^1.5.2",
+        "debug": "^2.2.0",
+        "mkdirp": "0.5.x"
+      },
+      "dependencies": {
+        "async": {
+          "version": "1.5.2",
+          "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+          "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
+          "dev": true
+        },
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "posix-character-classes": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+      "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
+      "dev": true
+    },
+    "postcss": {
+      "version": "5.2.18",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
+      "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "js-base64": "^2.1.9",
+        "source-map": "^0.5.6",
+        "supports-color": "^3.2.3"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          },
+          "dependencies": {
+            "supports-color": {
+              "version": "2.0.0",
+              "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+              "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+              "dev": true
+            }
+          }
+        },
+        "has-ansi": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+          "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "has-flag": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+          "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "3.2.3",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+          "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+          "dev": true,
+          "requires": {
+            "has-flag": "^1.0.0"
+          }
+        }
+      }
+    },
+    "postcss-calc": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-5.3.1.tgz",
+      "integrity": "sha1-d7rnypKK2FcW4v2kLyYb98HWW14=",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.2",
+        "postcss-message-helpers": "^2.0.0",
+        "reduce-css-calc": "^1.2.6"
+      }
+    },
+    "postcss-colormin": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.2.tgz",
+      "integrity": "sha1-ZjFBfV8OkJo9fsJrJMio0eT5bks=",
+      "dev": true,
+      "requires": {
+        "colormin": "^1.0.5",
+        "postcss": "^5.0.13",
+        "postcss-value-parser": "^3.2.3"
+      }
+    },
+    "postcss-convert-values": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz",
+      "integrity": "sha1-u9hZPFwf0uPRwyK7kl3K6Nrk1i0=",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.11",
+        "postcss-value-parser": "^3.1.2"
+      }
+    },
+    "postcss-discard-comments": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz",
+      "integrity": "sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0=",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.14"
+      }
+    },
+    "postcss-discard-duplicates": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz",
+      "integrity": "sha1-uavye4isGIFYpesSq8riAmO5GTI=",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.4"
+      }
+    },
+    "postcss-discard-empty": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz",
+      "integrity": "sha1-0rS9nVztXr2Nyt52QMfXzX9PkrU=",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.14"
+      }
+    },
+    "postcss-discard-overridden": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz",
+      "integrity": "sha1-ix6vVU9ob7KIzYdMVWZ7CqNmjVg=",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.16"
+      }
+    },
+    "postcss-discard-unused": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz",
+      "integrity": "sha1-vOMLLMWR/8Y0Mitfs0ZLbZNPRDM=",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.14",
+        "uniqs": "^2.0.0"
+      }
+    },
+    "postcss-filter-plugins": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-2.0.3.tgz",
+      "integrity": "sha512-T53GVFsdinJhgwm7rg1BzbeBRomOg9y5MBVhGcsV0CxurUdVj1UlPdKtn7aqYA/c/QVkzKMjq2bSV5dKG5+AwQ==",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.4"
+      }
+    },
+    "postcss-merge-idents": {
+      "version": "2.1.7",
+      "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz",
+      "integrity": "sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=",
+      "dev": true,
+      "requires": {
+        "has": "^1.0.1",
+        "postcss": "^5.0.10",
+        "postcss-value-parser": "^3.1.1"
+      }
+    },
+    "postcss-merge-longhand": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz",
+      "integrity": "sha1-I9kM0Sewp3mUkVMyc5A0oaTz1lg=",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.4"
+      }
+    },
+    "postcss-merge-rules": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz",
+      "integrity": "sha1-0d9d+qexrMO+VT8OnhDofGG19yE=",
+      "dev": true,
+      "requires": {
+        "browserslist": "^1.5.2",
+        "caniuse-api": "^1.5.2",
+        "postcss": "^5.0.4",
+        "postcss-selector-parser": "^2.2.2",
+        "vendors": "^1.0.0"
+      },
+      "dependencies": {
+        "browserslist": {
+          "version": "1.7.7",
+          "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz",
+          "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=",
+          "dev": true,
+          "requires": {
+            "caniuse-db": "^1.0.30000639",
+            "electron-to-chromium": "^1.2.7"
+          }
+        }
+      }
+    },
+    "postcss-message-helpers": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz",
+      "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=",
+      "dev": true
+    },
+    "postcss-minify-font-values": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz",
+      "integrity": "sha1-S1jttWZB66fIR0qzUmyv17vey2k=",
+      "dev": true,
+      "requires": {
+        "object-assign": "^4.0.1",
+        "postcss": "^5.0.4",
+        "postcss-value-parser": "^3.0.2"
+      }
+    },
+    "postcss-minify-gradients": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz",
+      "integrity": "sha1-Xb2hE3NwP4PPtKPqOIHY11/15uE=",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.12",
+        "postcss-value-parser": "^3.3.0"
+      }
+    },
+    "postcss-minify-params": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz",
+      "integrity": "sha1-rSzgcTc7lDs9kwo/pZo1jCjW8fM=",
+      "dev": true,
+      "requires": {
+        "alphanum-sort": "^1.0.1",
+        "postcss": "^5.0.2",
+        "postcss-value-parser": "^3.0.2",
+        "uniqs": "^2.0.0"
+      }
+    },
+    "postcss-minify-selectors": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz",
+      "integrity": "sha1-ssapjAByz5G5MtGkllCBFDEXNb8=",
+      "dev": true,
+      "requires": {
+        "alphanum-sort": "^1.0.2",
+        "has": "^1.0.1",
+        "postcss": "^5.0.14",
+        "postcss-selector-parser": "^2.0.0"
+      }
+    },
+    "postcss-modules-extract-imports": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz",
+      "integrity": "sha512-6jt9XZwUhwmRUhb/CkyJY020PYaPJsCyt3UjbaWo6XEbH/94Hmv6MP7fG2C5NDU/BcHzyGYxNtHvM+LTf9HrYw==",
+      "dev": true,
+      "requires": {
+        "postcss": "^6.0.1"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "postcss": {
+          "version": "6.0.23",
+          "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+          "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.4.1",
+            "source-map": "^0.6.1",
+            "supports-color": "^5.4.0"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "postcss-modules-local-by-default": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz",
+      "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=",
+      "dev": true,
+      "requires": {
+        "css-selector-tokenizer": "^0.7.0",
+        "postcss": "^6.0.1"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "postcss": {
+          "version": "6.0.23",
+          "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+          "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.4.1",
+            "source-map": "^0.6.1",
+            "supports-color": "^5.4.0"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "postcss-modules-scope": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz",
+      "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=",
+      "dev": true,
+      "requires": {
+        "css-selector-tokenizer": "^0.7.0",
+        "postcss": "^6.0.1"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "postcss": {
+          "version": "6.0.23",
+          "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+          "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.4.1",
+            "source-map": "^0.6.1",
+            "supports-color": "^5.4.0"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "postcss-modules-values": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz",
+      "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=",
+      "dev": true,
+      "requires": {
+        "icss-replace-symbols": "^1.1.0",
+        "postcss": "^6.0.1"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "postcss": {
+          "version": "6.0.23",
+          "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+          "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.4.1",
+            "source-map": "^0.6.1",
+            "supports-color": "^5.4.0"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "postcss-normalize-charset": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz",
+      "integrity": "sha1-757nEhLX/nWceO0WL2HtYrXLk/E=",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.5"
+      }
+    },
+    "postcss-normalize-url": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz",
+      "integrity": "sha1-EI90s/L82viRov+j6kWSJ5/HgiI=",
+      "dev": true,
+      "requires": {
+        "is-absolute-url": "^2.0.0",
+        "normalize-url": "^1.4.0",
+        "postcss": "^5.0.14",
+        "postcss-value-parser": "^3.2.3"
+      }
+    },
+    "postcss-ordered-values": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz",
+      "integrity": "sha1-7sbCpntsQSqNsgQud/6NpD+VwR0=",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.4",
+        "postcss-value-parser": "^3.0.1"
+      }
+    },
+    "postcss-prefix-selector": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/postcss-prefix-selector/-/postcss-prefix-selector-1.6.0.tgz",
+      "integrity": "sha1-tJWUnWOcYxRxRWSDJoUyFvPBCQA=",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.8"
+      }
+    },
+    "postcss-reduce-idents": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz",
+      "integrity": "sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM=",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.4",
+        "postcss-value-parser": "^3.0.2"
+      }
+    },
+    "postcss-reduce-initial": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz",
+      "integrity": "sha1-aPgGlfBF0IJjqHmtJA343WT2ROo=",
+      "dev": true,
+      "requires": {
+        "postcss": "^5.0.4"
+      }
+    },
+    "postcss-reduce-transforms": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz",
+      "integrity": "sha1-/3b02CEkN7McKYpC0uFEQCV3GuE=",
+      "dev": true,
+      "requires": {
+        "has": "^1.0.1",
+        "postcss": "^5.0.8",
+        "postcss-value-parser": "^3.0.1"
+      }
+    },
+    "postcss-selector-parser": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz",
+      "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=",
+      "dev": true,
+      "requires": {
+        "flatten": "^1.0.2",
+        "indexes-of": "^1.0.1",
+        "uniq": "^1.0.1"
+      }
+    },
+    "postcss-svgo": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-2.1.6.tgz",
+      "integrity": "sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0=",
+      "dev": true,
+      "requires": {
+        "is-svg": "^2.0.0",
+        "postcss": "^5.0.14",
+        "postcss-value-parser": "^3.2.3",
+        "svgo": "^0.7.0"
+      }
+    },
+    "postcss-unique-selectors": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz",
+      "integrity": "sha1-mB1X0p3csz57Hf4f1DuGSfkzyh0=",
+      "dev": true,
+      "requires": {
+        "alphanum-sort": "^1.0.1",
+        "postcss": "^5.0.4",
+        "uniqs": "^2.0.0"
+      }
+    },
+    "postcss-value-parser": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+      "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+      "dev": true
+    },
+    "postcss-zindex": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.2.0.tgz",
+      "integrity": "sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=",
+      "dev": true,
+      "requires": {
+        "has": "^1.0.1",
+        "postcss": "^5.0.4",
+        "uniqs": "^2.0.0"
+      }
+    },
+    "posthtml": {
+      "version": "0.11.3",
+      "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.11.3.tgz",
+      "integrity": "sha512-quMHnDckt2DQ9lRi6bYLnuyBDnVzK+McHa8+ar4kTdYbWEo/92hREOu3h70ZirudOOp/my2b3r0m5YtxY52yrA==",
+      "dev": true,
+      "requires": {
+        "object-assign": "^4.1.1",
+        "posthtml-parser": "^0.3.3",
+        "posthtml-render": "^1.1.0"
+      }
+    },
+    "posthtml-parser": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.3.3.tgz",
+      "integrity": "sha512-H/Z/yXGwl49A7hYQLV1iQ3h87NE0aZ/PMZhFwhw3lKeCAN+Ti4idrHvVvh4/GX10I7u77aQw+QB4vV5/Lzvv5A==",
+      "dev": true,
+      "requires": {
+        "htmlparser2": "^3.9.2",
+        "isobject": "^2.1.0",
+        "object-assign": "^4.1.1"
+      },
+      "dependencies": {
+        "isobject": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+          "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+          "dev": true,
+          "requires": {
+            "isarray": "1.0.0"
+          }
+        }
+      }
+    },
+    "posthtml-rename-id": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/posthtml-rename-id/-/posthtml-rename-id-1.0.11.tgz",
+      "integrity": "sha512-8doF8+w+WJT4AZuLVC0feA8Yy7g00IUmZw3YDKn8CKx0uC8FLbCH7JaGMbDOE1ArjyZsJMt1vmyP+IZ8SnNmXw==",
+      "dev": true,
+      "requires": {
+        "escape-string-regexp": "1.0.5"
+      }
+    },
+    "posthtml-render": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-1.1.4.tgz",
+      "integrity": "sha512-jL6eFIzoN3xUEvbo33OAkSDE2VIKU4JQ1wENOows1DpfnrdapR/K3Q1/fB43Mq7wQlcSgRm23nFrvoioufM7eA==",
+      "dev": true
+    },
+    "posthtml-svg-mode": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/posthtml-svg-mode/-/posthtml-svg-mode-1.0.3.tgz",
+      "integrity": "sha512-hEqw9NHZ9YgJ2/0G7CECOeuLQKZi8HjWLkBaSVtOWjygQ9ZD8P7tqeowYs7WrFdKsWEKG7o+IlsPY8jrr0CJpQ==",
+      "dev": true,
+      "requires": {
+        "merge-options": "1.0.1",
+        "posthtml": "^0.9.2",
+        "posthtml-parser": "^0.2.1",
+        "posthtml-render": "^1.0.6"
+      },
+      "dependencies": {
+        "isobject": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+          "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+          "dev": true,
+          "requires": {
+            "isarray": "1.0.0"
+          }
+        },
+        "posthtml": {
+          "version": "0.9.2",
+          "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.9.2.tgz",
+          "integrity": "sha1-9MBtufZ7Yf0XxOJW5+PZUVv3Jv0=",
+          "dev": true,
+          "requires": {
+            "posthtml-parser": "^0.2.0",
+            "posthtml-render": "^1.0.5"
+          }
+        },
+        "posthtml-parser": {
+          "version": "0.2.1",
+          "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.2.1.tgz",
+          "integrity": "sha1-NdUw3jhnQMK6JP8usvrznM3ycd0=",
+          "dev": true,
+          "requires": {
+            "htmlparser2": "^3.8.3",
+            "isobject": "^2.1.0"
+          }
+        }
+      }
+    },
+    "prelude-ls": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+      "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+      "dev": true
+    },
+    "prepend-http": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
+      "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
+      "dev": true
+    },
+    "pretty-error": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz",
+      "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=",
+      "dev": true,
+      "requires": {
+        "renderkid": "^2.0.1",
+        "utila": "~0.4"
+      }
+    },
+    "private": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
+      "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==",
+      "dev": true
+    },
+    "process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
+      "dev": true
+    },
+    "process-nextick-args": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
+      "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
+    },
+    "progress": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz",
+      "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=",
+      "dev": true
+    },
+    "progress-bar-webpack-plugin": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/progress-bar-webpack-plugin/-/progress-bar-webpack-plugin-1.11.0.tgz",
+      "integrity": "sha512-XT6r8strD6toU0ZVip25baJINo7uE4BD4H8d4vhOV4GIK5PvNNky8GYJ2wMmVoYP8eo/sSmtNWn0Vw7zWDDE3A==",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.1",
+        "object.assign": "^4.0.1",
+        "progress": "^1.1.8"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "has-ansi": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+          "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "progress": {
+          "version": "1.1.8",
+          "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz",
+          "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
+    "promise": {
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
+      "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
+      "dev": true,
+      "requires": {
+        "asap": "~2.0.3"
+      }
+    },
+    "promise-inflight": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+      "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
+      "dev": true
+    },
+    "proxy-addr": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
+      "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
+      "dev": true,
+      "requires": {
+        "forwarded": "~0.1.2",
+        "ipaddr.js": "1.8.0"
+      }
+    },
+    "prr": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
+      "dev": true
+    },
+    "pseudomap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+      "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
+      "dev": true
+    },
+    "psl": {
+      "version": "1.1.31",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
+      "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==",
+      "dev": true
+    },
+    "public-encrypt": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
+      "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "browserify-rsa": "^4.0.0",
+        "create-hash": "^1.1.0",
+        "parse-asn1": "^5.0.0",
+        "randombytes": "^2.0.1",
+        "safe-buffer": "^5.1.2"
+      }
+    },
+    "pug": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.3.tgz",
+      "integrity": "sha1-ccuoJTfJWl6rftBGluQiH1Oqh44=",
+      "dev": true,
+      "requires": {
+        "pug-code-gen": "^2.0.1",
+        "pug-filters": "^3.1.0",
+        "pug-lexer": "^4.0.0",
+        "pug-linker": "^3.0.5",
+        "pug-load": "^2.0.11",
+        "pug-parser": "^5.0.0",
+        "pug-runtime": "^2.0.4",
+        "pug-strip-comments": "^1.0.3"
+      }
+    },
+    "pug-attrs": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.3.tgz",
+      "integrity": "sha1-owlflw5kFR972tlX7vVftdeQXRU=",
+      "dev": true,
+      "requires": {
+        "constantinople": "^3.0.1",
+        "js-stringify": "^1.0.1",
+        "pug-runtime": "^2.0.4"
+      }
+    },
+    "pug-code-gen": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.1.tgz",
+      "integrity": "sha1-CVHsgyJddNjPxHan+Zolm199BQw=",
+      "dev": true,
+      "requires": {
+        "constantinople": "^3.0.1",
+        "doctypes": "^1.1.0",
+        "js-stringify": "^1.0.1",
+        "pug-attrs": "^2.0.3",
+        "pug-error": "^1.3.2",
+        "pug-runtime": "^2.0.4",
+        "void-elements": "^2.0.1",
+        "with": "^5.0.0"
+      }
+    },
+    "pug-error": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz",
+      "integrity": "sha1-U659nSm7A89WRJOgJhCfVMR/XyY=",
+      "dev": true
+    },
+    "pug-filters": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.0.tgz",
+      "integrity": "sha1-JxZVVbwEwjbkqisDZiRt+gIbYm4=",
+      "dev": true,
+      "requires": {
+        "clean-css": "^4.1.11",
+        "constantinople": "^3.0.1",
+        "jstransformer": "1.0.0",
+        "pug-error": "^1.3.2",
+        "pug-walk": "^1.1.7",
+        "resolve": "^1.1.6",
+        "uglify-js": "^2.6.1"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
+          "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
+          "dev": true
+        },
+        "cliui": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
+          "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=",
+          "dev": true,
+          "requires": {
+            "center-align": "^0.1.1",
+            "right-align": "^0.1.1",
+            "wordwrap": "0.0.2"
+          }
+        },
+        "uglify-js": {
+          "version": "2.8.29",
+          "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
+          "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
+          "dev": true,
+          "requires": {
+            "source-map": "~0.5.1",
+            "uglify-to-browserify": "~1.0.0",
+            "yargs": "~3.10.0"
+          }
+        },
+        "wordwrap": {
+          "version": "0.0.2",
+          "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
+          "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=",
+          "dev": true
+        },
+        "yargs": {
+          "version": "3.10.0",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
+          "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
+          "dev": true,
+          "requires": {
+            "camelcase": "^1.0.2",
+            "cliui": "^2.1.0",
+            "decamelize": "^1.0.0",
+            "window-size": "0.1.0"
+          }
+        }
+      }
+    },
+    "pug-html-loader": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/pug-html-loader/-/pug-html-loader-1.1.0.tgz",
+      "integrity": "sha1-w5aGShAgQfZvYqmgGTBdWTMZydI=",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^0.2.14",
+        "pug": "^2.0.0-beta6"
+      },
+      "dependencies": {
+        "big.js": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
+          "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==",
+          "dev": true
+        },
+        "loader-utils": {
+          "version": "0.2.17",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz",
+          "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=",
+          "dev": true,
+          "requires": {
+            "big.js": "^3.1.3",
+            "emojis-list": "^2.0.0",
+            "json5": "^0.5.0",
+            "object-assign": "^4.0.1"
+          }
+        }
+      }
+    },
+    "pug-lexer": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.0.0.tgz",
+      "integrity": "sha1-IQwYRX7y4XYCQnQMXmR715TOwng=",
+      "dev": true,
+      "requires": {
+        "character-parser": "^2.1.1",
+        "is-expression": "^3.0.0",
+        "pug-error": "^1.3.2"
+      }
+    },
+    "pug-linker": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.5.tgz",
+      "integrity": "sha1-npp65ABWgtAn3uuWsAD4juuDoC8=",
+      "dev": true,
+      "requires": {
+        "pug-error": "^1.3.2",
+        "pug-walk": "^1.1.7"
+      }
+    },
+    "pug-load": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.11.tgz",
+      "integrity": "sha1-5kjlftET/iwfRdV4WOorrWvAFSc=",
+      "dev": true,
+      "requires": {
+        "object-assign": "^4.1.0",
+        "pug-walk": "^1.1.7"
+      }
+    },
+    "pug-loader": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/pug-loader/-/pug-loader-2.4.0.tgz",
+      "integrity": "sha512-cD4bU2wmkZ1EEVyu0IfKOsh1F26KPva5oglO1Doc3knx8VpBIXmFHw16k9sITYIjQMCnRv1vb4vfQgy7VdR6eg==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "pug-walk": "^1.0.0",
+        "resolve": "^1.1.7"
+      }
+    },
+    "pug-parser": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.0.tgz",
+      "integrity": "sha1-45Stmz/KkxI5QK/4hcBuRKt+aOQ=",
+      "dev": true,
+      "requires": {
+        "pug-error": "^1.3.2",
+        "token-stream": "0.0.1"
+      }
+    },
+    "pug-runtime": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.4.tgz",
+      "integrity": "sha1-4XjhvaaKsujArPybztLFT9iM61g=",
+      "dev": true
+    },
+    "pug-strip-comments": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.3.tgz",
+      "integrity": "sha1-8VWVkiBu3G+FMQ2s9K+0igJa9Z8=",
+      "dev": true,
+      "requires": {
+        "pug-error": "^1.3.2"
+      }
+    },
+    "pug-walk": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.7.tgz",
+      "integrity": "sha1-wA1cUSi6xYBr7BXSt+fNq+QlMfM=",
+      "dev": true
+    },
+    "pump": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
+      "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
+      "dev": true,
+      "requires": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "pumpify": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz",
+      "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
+      "dev": true,
+      "requires": {
+        "duplexify": "^3.6.0",
+        "inherits": "^2.0.3",
+        "pump": "^2.0.0"
+      }
+    },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+      "dev": true
+    },
+    "q": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
+      "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
+      "dev": true
+    },
+    "qjobs": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz",
+      "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==",
+      "dev": true
+    },
+    "qs": {
+      "version": "6.5.2",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+      "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+      "dev": true
+    },
+    "query-string": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
+      "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
+      "dev": true,
+      "requires": {
+        "object-assign": "^4.1.0",
+        "strict-uri-encode": "^1.0.0"
+      }
+    },
+    "querystring": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
+      "dev": true
+    },
+    "querystring-es3": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+      "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
+      "dev": true
+    },
+    "querystringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.0.tgz",
+      "integrity": "sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg==",
+      "dev": true
+    },
+    "randombytes": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz",
+      "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "randomfill": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
+      "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
+      "dev": true,
+      "requires": {
+        "randombytes": "^2.0.5",
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "range-parser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+      "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=",
+      "dev": true
+    },
+    "raw-body": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
+      "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
+      "dev": true,
+      "requires": {
+        "bytes": "3.0.0",
+        "http-errors": "1.6.3",
+        "iconv-lite": "0.4.23",
+        "unpipe": "1.0.0"
+      },
+      "dependencies": {
+        "iconv-lite": {
+          "version": "0.4.23",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
+          "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
+          "dev": true,
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3"
+          }
+        }
+      }
+    },
+    "read-pkg": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+      "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+      "dev": true,
+      "requires": {
+        "load-json-file": "^1.0.0",
+        "normalize-package-data": "^2.3.2",
+        "path-type": "^1.0.0"
+      },
+      "dependencies": {
+        "path-type": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+          "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "^4.1.2",
+            "pify": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          }
+        },
+        "pify": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+          "dev": true
+        }
+      }
+    },
+    "read-pkg-up": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+      "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+      "dev": true,
+      "requires": {
+        "find-up": "^1.0.0",
+        "read-pkg": "^1.0.0"
+      },
+      "dependencies": {
+        "find-up": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+          "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+          "dev": true,
+          "requires": {
+            "path-exists": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          }
+        },
+        "path-exists": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+          "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+          "dev": true,
+          "requires": {
+            "pinkie-promise": "^2.0.0"
+          }
+        }
+      }
+    },
+    "readable-stream": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
+      "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
+      "requires": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.1",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~1.0.6",
+        "string_decoder": "~0.10.x",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "readdirp": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+      "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.11",
+        "micromatch": "^3.1.10",
+        "readable-stream": "^2.0.2"
+      }
+    },
+    "redent": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+      "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+      "dev": true,
+      "requires": {
+        "indent-string": "^2.1.0",
+        "strip-indent": "^1.0.1"
+      }
+    },
+    "reduce-css-calc": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz",
+      "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^0.4.2",
+        "math-expression-evaluator": "^1.2.14",
+        "reduce-function-call": "^1.0.1"
+      },
+      "dependencies": {
+        "balanced-match": {
+          "version": "0.4.2",
+          "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
+          "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=",
+          "dev": true
+        }
+      }
+    },
+    "reduce-function-call": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz",
+      "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^0.4.2"
+      },
+      "dependencies": {
+        "balanced-match": {
+          "version": "0.4.2",
+          "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
+          "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=",
+          "dev": true
+        }
+      }
+    },
+    "regenerate": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
+      "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==",
+      "dev": true
+    },
+    "regenerate-unicode-properties": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz",
+      "integrity": "sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw==",
+      "dev": true,
+      "requires": {
+        "regenerate": "^1.4.0"
+      }
+    },
+    "regenerator-runtime": {
+      "version": "0.11.1",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+      "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
+      "dev": true
+    },
+    "regenerator-transform": {
+      "version": "0.13.3",
+      "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.13.3.tgz",
+      "integrity": "sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA==",
+      "dev": true,
+      "requires": {
+        "private": "^0.1.6"
+      }
+    },
+    "regex-not": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+      "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^3.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "regex-parser": {
+      "version": "2.2.10",
+      "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.10.tgz",
+      "integrity": "sha512-8t6074A68gHfU8Neftl0Le6KTDwfGAj7IyjPIMSfikI2wJUTHDMaIq42bUsfVnj8mhx0R+45rdUXHGpN164avA==",
+      "dev": true
+    },
+    "regexpp": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
+      "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+      "dev": true
+    },
+    "regexpu-core": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.4.0.tgz",
+      "integrity": "sha512-eDDWElbwwI3K0Lo6CqbQbA6FwgtCz4kYTarrri1okfkRLZAqstU+B3voZBCjg8Fl6iq0gXrJG6MvRgLthfvgOA==",
+      "dev": true,
+      "requires": {
+        "regenerate": "^1.4.0",
+        "regenerate-unicode-properties": "^7.0.0",
+        "regjsgen": "^0.5.0",
+        "regjsparser": "^0.6.0",
+        "unicode-match-property-ecmascript": "^1.0.4",
+        "unicode-match-property-value-ecmascript": "^1.0.2"
+      }
+    },
+    "regjsgen": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz",
+      "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==",
+      "dev": true
+    },
+    "regjsparser": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz",
+      "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==",
+      "dev": true,
+      "requires": {
+        "jsesc": "~0.5.0"
+      },
+      "dependencies": {
+        "jsesc": {
+          "version": "0.5.0",
+          "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+          "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
+          "dev": true
+        }
+      }
+    },
+    "relateurl": {
+      "version": "0.2.7",
+      "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
+      "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
+      "dev": true
+    },
+    "remove-trailing-separator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+      "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+      "dev": true
+    },
+    "renderkid": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.2.tgz",
+      "integrity": "sha512-FsygIxevi1jSiPY9h7vZmBFUbAOcbYm9UwyiLNdVsLRs/5We9Ob5NMPbGYUTWiLq5L+ezlVdE0A8bbME5CWTpg==",
+      "dev": true,
+      "requires": {
+        "css-select": "^1.1.0",
+        "dom-converter": "~0.2",
+        "htmlparser2": "~3.3.0",
+        "strip-ansi": "^3.0.0",
+        "utila": "^0.4.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "css-select": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
+          "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
+          "dev": true,
+          "requires": {
+            "boolbase": "~1.0.0",
+            "css-what": "2.1",
+            "domutils": "1.5.1",
+            "nth-check": "~1.0.1"
+          }
+        },
+        "domhandler": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz",
+          "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=",
+          "dev": true,
+          "requires": {
+            "domelementtype": "1"
+          }
+        },
+        "domutils": {
+          "version": "1.5.1",
+          "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
+          "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
+          "dev": true,
+          "requires": {
+            "dom-serializer": "0",
+            "domelementtype": "1"
+          }
+        },
+        "htmlparser2": {
+          "version": "3.3.0",
+          "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz",
+          "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=",
+          "dev": true,
+          "requires": {
+            "domelementtype": "1",
+            "domhandler": "2.1",
+            "domutils": "1.1",
+            "readable-stream": "1.0"
+          },
+          "dependencies": {
+            "domutils": {
+              "version": "1.1.6",
+              "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz",
+              "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=",
+              "dev": true,
+              "requires": {
+                "domelementtype": "1"
+              }
+            }
+          }
+        },
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "1.0.34",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+          "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "0.0.1",
+            "string_decoder": "~0.10.x"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        }
+      }
+    },
+    "repeat-element": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
+      "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==",
+      "dev": true
+    },
+    "repeat-string": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+      "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+      "dev": true
+    },
+    "repeating": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+      "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+      "dev": true,
+      "requires": {
+        "is-finite": "^1.0.0"
+      }
+    },
+    "request": {
+      "version": "2.88.0",
+      "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
+      "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+      "dev": true,
+      "requires": {
+        "aws-sign2": "~0.7.0",
+        "aws4": "^1.8.0",
+        "caseless": "~0.12.0",
+        "combined-stream": "~1.0.6",
+        "extend": "~3.0.2",
+        "forever-agent": "~0.6.1",
+        "form-data": "~2.3.2",
+        "har-validator": "~5.1.0",
+        "http-signature": "~1.2.0",
+        "is-typedarray": "~1.0.0",
+        "isstream": "~0.1.2",
+        "json-stringify-safe": "~5.0.1",
+        "mime-types": "~2.1.19",
+        "oauth-sign": "~0.9.0",
+        "performance-now": "^2.1.0",
+        "qs": "~6.5.2",
+        "safe-buffer": "^5.1.2",
+        "tough-cookie": "~2.4.3",
+        "tunnel-agent": "^0.6.0",
+        "uuid": "^3.3.2"
+      }
+    },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+      "dev": true
+    },
+    "require-main-filename": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+      "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+      "dev": true
+    },
+    "requireindex": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
+      "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
+      "dev": true
+    },
+    "requires-port": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+      "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
+      "dev": true
+    },
+    "resize-observer-polyfill": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.0.tgz",
+      "integrity": "sha512-M2AelyJDVR/oLnToJLtuDJRBBWUGUvvGigj1411hXhAdyFWqMaqHp7TixW3FpiLuVaikIcR1QL+zqoJoZlOgpg=="
+    },
+    "resolve": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz",
+      "integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==",
+      "dev": true,
+      "requires": {
+        "path-parse": "^1.0.6"
+      }
+    },
+    "resolve-cwd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz",
+      "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=",
+      "dev": true,
+      "requires": {
+        "resolve-from": "^3.0.0"
+      },
+      "dependencies": {
+        "resolve-from": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
+          "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
+          "dev": true
+        }
+      }
+    },
+    "resolve-dir": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+      "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=",
+      "dev": true,
+      "requires": {
+        "expand-tilde": "^2.0.0",
+        "global-modules": "^1.0.0"
+      }
+    },
+    "resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true
+    },
+    "resolve-url": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+      "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
+      "dev": true
+    },
+    "resolve-url-loader": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-2.1.0.tgz",
+      "integrity": "sha1-J8lcwWpDU5I/29wtuvXu8iIyxHc=",
+      "dev": true,
+      "requires": {
+        "adjust-sourcemap-loader": "^1.1.0",
+        "camelcase": "^4.0.0",
+        "convert-source-map": "^1.1.1",
+        "loader-utils": "^1.0.0",
+        "lodash.defaults": "^4.0.0",
+        "rework": "^1.0.1",
+        "rework-visit": "^1.0.0",
+        "source-map": "^0.5.6",
+        "urix": "^0.1.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+          "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
+          "dev": true
+        }
+      }
+    },
+    "restore-cursor": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+      "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+      "dev": true,
+      "requires": {
+        "onetime": "^2.0.0",
+        "signal-exit": "^3.0.2"
+      }
+    },
+    "ret": {
+      "version": "0.1.15",
+      "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+      "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+      "dev": true
+    },
+    "rework": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz",
+      "integrity": "sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=",
+      "dev": true,
+      "requires": {
+        "convert-source-map": "^0.3.3",
+        "css": "^2.0.0"
+      },
+      "dependencies": {
+        "convert-source-map": {
+          "version": "0.3.5",
+          "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz",
+          "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=",
+          "dev": true
+        }
+      }
+    },
+    "rework-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/rework-visit/-/rework-visit-1.0.0.tgz",
+      "integrity": "sha1-mUWygD8hni96ygCtuLyfZA+ELJo=",
+      "dev": true
+    },
+    "rfdc": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.2.tgz",
+      "integrity": "sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA==",
+      "dev": true
+    },
+    "right-align": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz",
+      "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=",
+      "dev": true,
+      "requires": {
+        "align-text": "^0.1.1"
+      }
+    },
+    "rimraf": {
+      "version": "2.6.3",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+      "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.3"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+          "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        }
+      }
+    },
+    "ripemd160": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
+      "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+      "dev": true,
+      "requires": {
+        "hash-base": "^3.0.0",
+        "inherits": "^2.0.1"
+      }
+    },
+    "roboto-font": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/roboto-font/-/roboto-font-0.1.0.tgz",
+      "integrity": "sha1-w+4Z2Cygh7x0JCPA+ZdDhFVeGf0="
+    },
+    "run-async": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
+      "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+      "dev": true,
+      "requires": {
+        "is-promise": "^2.1.0"
+      }
+    },
+    "run-queue": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
+      "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=",
+      "dev": true,
+      "requires": {
+        "aproba": "^1.1.1"
+      }
+    },
+    "rxjs": {
+      "version": "6.3.3",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz",
+      "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
+    "safe-regex": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+      "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+      "dev": true,
+      "requires": {
+        "ret": "~0.1.10"
+      }
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "dev": true
+    },
+    "samsam": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz",
+      "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==",
+      "dev": true
+    },
+    "sass-graph": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",
+      "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=",
+      "dev": true,
+      "requires": {
+        "glob": "^7.0.0",
+        "lodash": "^4.0.0",
+        "scss-tokenizer": "^0.2.3",
+        "yargs": "^7.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "camelcase": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+          "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+          "dev": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "dev": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "y18n": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+          "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
+          "dev": true
+        },
+        "yargs": {
+          "version": "7.1.0",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
+          "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
+          "dev": true,
+          "requires": {
+            "camelcase": "^3.0.0",
+            "cliui": "^3.2.0",
+            "decamelize": "^1.1.1",
+            "get-caller-file": "^1.0.1",
+            "os-locale": "^1.4.0",
+            "read-pkg-up": "^1.0.1",
+            "require-directory": "^2.1.1",
+            "require-main-filename": "^1.0.1",
+            "set-blocking": "^2.0.0",
+            "string-width": "^1.0.2",
+            "which-module": "^1.0.0",
+            "y18n": "^3.2.1",
+            "yargs-parser": "^5.0.0"
+          }
+        }
+      }
+    },
+    "sass-loader": {
+      "version": "6.0.7",
+      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-6.0.7.tgz",
+      "integrity": "sha512-JoiyD00Yo1o61OJsoP2s2kb19L1/Y2p3QFcCdWdF6oomBGKVYuZyqHWemRBfQ2uGYsk+CH3eCguXNfpjzlcpaA==",
+      "dev": true,
+      "requires": {
+        "clone-deep": "^2.0.1",
+        "loader-utils": "^1.0.1",
+        "lodash.tail": "^4.1.1",
+        "neo-async": "^2.5.0",
+        "pify": "^3.0.0"
+      }
+    },
+    "sax": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+      "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+      "dev": true
+    },
+    "schema-utils": {
+      "version": "0.4.7",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz",
+      "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.1.0",
+        "ajv-keywords": "^3.1.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.6.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz",
+          "integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^2.0.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "fast-deep-equal": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+          "dev": true
+        },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        }
+      }
+    },
+    "scss-tokenizer": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
+      "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=",
+      "dev": true,
+      "requires": {
+        "js-base64": "^2.1.8",
+        "source-map": "^0.4.2"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.4.4",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+          "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+          "dev": true,
+          "requires": {
+            "amdefine": ">=0.0.4"
+          }
+        }
+      }
+    },
+    "select-hose": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
+      "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=",
+      "dev": true
+    },
+    "selfsigned": {
+      "version": "1.10.4",
+      "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.4.tgz",
+      "integrity": "sha512-9AukTiDmHXGXWtWjembZ5NDmVvP2695EtpgbCsxCa68w3c88B+alqbmZ4O3hZ4VWGXeGWzEVdvqgAJD8DQPCDw==",
+      "dev": true,
+      "requires": {
+        "node-forge": "0.7.5"
+      }
+    },
+    "semver": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
+      "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==",
+      "dev": true
+    },
+    "send": {
+      "version": "0.16.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
+      "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "destroy": "~1.0.4",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "~1.6.2",
+        "mime": "1.4.1",
+        "ms": "2.0.0",
+        "on-finished": "~2.3.0",
+        "range-parser": "~1.2.0",
+        "statuses": "~1.4.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "mime": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
+          "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==",
+          "dev": true
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        },
+        "statuses": {
+          "version": "1.4.0",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+          "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==",
+          "dev": true
+        }
+      }
+    },
+    "serialize-javascript": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.6.1.tgz",
+      "integrity": "sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw==",
+      "dev": true
+    },
+    "serve-index": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
+      "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=",
+      "dev": true,
+      "requires": {
+        "accepts": "~1.3.4",
+        "batch": "0.6.1",
+        "debug": "2.6.9",
+        "escape-html": "~1.0.3",
+        "http-errors": "~1.6.2",
+        "mime-types": "~2.1.17",
+        "parseurl": "~1.3.2"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "serve-static": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
+      "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+      "dev": true,
+      "requires": {
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.2",
+        "send": "0.16.2"
+      }
+    },
+    "set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+      "dev": true
+    },
+    "set-value": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
+      "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^2.0.1",
+        "is-extendable": "^0.1.1",
+        "is-plain-object": "^2.0.3",
+        "split-string": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
+      "dev": true
+    },
+    "setprototypeof": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+      "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
+      "dev": true
+    },
+    "sha.js": {
+      "version": "2.4.11",
+      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+      "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "shallow-clone": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz",
+      "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==",
+      "dev": true,
+      "requires": {
+        "is-extendable": "^0.1.1",
+        "kind-of": "^5.0.0",
+        "mixin-object": "^2.0.1"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+          "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+          "dev": true
+        }
+      }
+    },
+    "shebang-command": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+      "dev": true,
+      "requires": {
+        "shebang-regex": "^1.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+      "dev": true
+    },
+    "signal-exit": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+      "dev": true
+    },
+    "sinon": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.3.8.tgz",
+      "integrity": "sha1-Md4G/tj7o6Zx5XbdltClhjeW8lw=",
+      "dev": true,
+      "requires": {
+        "diff": "^3.1.0",
+        "formatio": "1.2.0",
+        "lolex": "^1.6.0",
+        "native-promise-only": "^0.8.1",
+        "path-to-regexp": "^1.7.0",
+        "samsam": "^1.1.3",
+        "text-encoding": "0.6.4",
+        "type-detect": "^4.0.0"
+      }
+    },
+    "slash": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
+      "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
+      "dev": true
+    },
+    "slice-ansi": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
+      "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^3.2.0",
+        "astral-regex": "^1.0.0",
+        "is-fullwidth-code-point": "^2.0.0"
+      }
+    },
+    "snapdragon": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+      "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+      "dev": true,
+      "requires": {
+        "base": "^0.11.1",
+        "debug": "^2.2.0",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "map-cache": "^0.2.2",
+        "source-map": "^0.5.6",
+        "source-map-resolve": "^0.5.0",
+        "use": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "snapdragon-node": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+      "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+      "dev": true,
+      "requires": {
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.0",
+        "snapdragon-util": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "snapdragon-util": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+      "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.2.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "socket.io": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz",
+      "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==",
+      "dev": true,
+      "requires": {
+        "debug": "~3.1.0",
+        "engine.io": "~3.2.0",
+        "has-binary2": "~1.0.2",
+        "socket.io-adapter": "~1.1.0",
+        "socket.io-client": "2.1.1",
+        "socket.io-parser": "~3.2.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "socket.io-adapter": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
+      "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=",
+      "dev": true
+    },
+    "socket.io-client": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz",
+      "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==",
+      "requires": {
+        "backo2": "1.0.2",
+        "base64-arraybuffer": "0.1.5",
+        "component-bind": "1.0.0",
+        "component-emitter": "1.2.1",
+        "debug": "~3.1.0",
+        "engine.io-client": "~3.2.0",
+        "has-binary2": "~1.0.2",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "object-component": "0.0.3",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "socket.io-parser": "~3.2.0",
+        "to-array": "0.1.4"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        }
+      }
+    },
+    "socket.io-parser": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz",
+      "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==",
+      "requires": {
+        "component-emitter": "1.2.1",
+        "debug": "~3.1.0",
+        "isarray": "2.0.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        }
+      }
+    },
+    "sockjs": {
+      "version": "0.3.19",
+      "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz",
+      "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==",
+      "dev": true,
+      "requires": {
+        "faye-websocket": "^0.10.0",
+        "uuid": "^3.0.1"
+      }
+    },
+    "sockjs-client": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.3.0.tgz",
+      "integrity": "sha512-R9jxEzhnnrdxLCNln0xg5uGHqMnkhPSTzUZH2eXcR03S/On9Yvoq2wyUZILRUhZCNVu2PmwWVoyuiPz8th8zbg==",
+      "dev": true,
+      "requires": {
+        "debug": "^3.2.5",
+        "eventsource": "^1.0.7",
+        "faye-websocket": "~0.11.1",
+        "inherits": "^2.0.3",
+        "json3": "^3.3.2",
+        "url-parse": "^1.4.3"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "faye-websocket": {
+          "version": "0.11.1",
+          "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz",
+          "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=",
+          "dev": true,
+          "requires": {
+            "websocket-driver": ">=0.5.1"
+          }
+        }
+      }
+    },
+    "sort-keys": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
+      "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
+      "dev": true,
+      "requires": {
+        "is-plain-obj": "^1.0.0"
+      }
+    },
+    "source-list-map": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
+      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
+      "dev": true
+    },
+    "source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
+    },
+    "source-map-resolve": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
+      "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==",
+      "dev": true,
+      "requires": {
+        "atob": "^2.1.1",
+        "decode-uri-component": "^0.2.0",
+        "resolve-url": "^0.2.1",
+        "source-map-url": "^0.4.0",
+        "urix": "^0.1.0"
+      }
+    },
+    "source-map-support": {
+      "version": "0.5.9",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz",
+      "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==",
+      "dev": true,
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "source-map-url": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
+      "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
+      "dev": true
+    },
+    "spdx-correct": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
+      "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
+      "dev": true,
+      "requires": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-exceptions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
+      "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
+      "dev": true
+    },
+    "spdx-expression-parse": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+      "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+      "dev": true,
+      "requires": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-license-ids": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz",
+      "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==",
+      "dev": true
+    },
+    "spdy": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.0.tgz",
+      "integrity": "sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q==",
+      "dev": true,
+      "requires": {
+        "debug": "^4.1.0",
+        "handle-thing": "^2.0.0",
+        "http-deceiver": "^1.2.7",
+        "select-hose": "^2.0.0",
+        "spdy-transport": "^3.0.0"
+      }
+    },
+    "spdy-transport": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz",
+      "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==",
+      "dev": true,
+      "requires": {
+        "debug": "^4.1.0",
+        "detect-node": "^2.0.4",
+        "hpack.js": "^2.1.6",
+        "obuf": "^1.1.2",
+        "readable-stream": "^3.0.6",
+        "wbuf": "^1.7.3"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz",
+          "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==",
+          "dev": true,
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz",
+          "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        }
+      }
+    },
+    "split-string": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+      "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^3.0.0"
+      }
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+      "dev": true
+    },
+    "sshpk": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz",
+      "integrity": "sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==",
+      "dev": true,
+      "requires": {
+        "asn1": "~0.2.3",
+        "assert-plus": "^1.0.0",
+        "bcrypt-pbkdf": "^1.0.0",
+        "dashdash": "^1.12.0",
+        "ecc-jsbn": "~0.1.1",
+        "getpass": "^0.1.1",
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.0.2",
+        "tweetnacl": "~0.14.0"
+      }
+    },
+    "ssri": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz",
+      "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "stable": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
+      "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
+      "dev": true
+    },
+    "static-extend": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+      "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+      "dev": true,
+      "requires": {
+        "define-property": "^0.2.5",
+        "object-copy": "^0.1.0"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
+    "statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
+      "dev": true
+    },
+    "stdout-stream": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz",
+      "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==",
+      "dev": true,
+      "requires": {
+        "readable-stream": "^2.0.1"
+      }
+    },
+    "stream-browserify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
+      "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
+      "dev": true,
+      "requires": {
+        "inherits": "~2.0.1",
+        "readable-stream": "^2.0.2"
+      }
+    },
+    "stream-each": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz",
+      "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==",
+      "dev": true,
+      "requires": {
+        "end-of-stream": "^1.1.0",
+        "stream-shift": "^1.0.0"
+      }
+    },
+    "stream-http": {
+      "version": "2.8.3",
+      "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz",
+      "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==",
+      "dev": true,
+      "requires": {
+        "builtin-status-codes": "^3.0.0",
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.3.6",
+        "to-arraybuffer": "^1.0.0",
+        "xtend": "^4.0.0"
+      },
+      "dependencies": {
+        "process-nextick-args": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+          "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        }
+      }
+    },
+    "stream-shift": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
+      "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
+      "dev": true
+    },
+    "streamroller": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.7.0.tgz",
+      "integrity": "sha512-WREzfy0r0zUqp3lGO096wRuUp7ho1X6uo/7DJfTlEi0Iv/4gT7YHqXDjKC2ioVGBZtE8QzsQD9nx1nIuoZ57jQ==",
+      "dev": true,
+      "requires": {
+        "date-format": "^1.2.0",
+        "debug": "^3.1.0",
+        "mkdirp": "^0.5.1",
+        "readable-stream": "^2.3.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "process-nextick-args": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+          "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        }
+      }
+    },
+    "strict-uri-encode": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
+      "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
+      "dev": true
+    },
+    "string-width": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+      "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+      "dev": true,
+      "requires": {
+        "is-fullwidth-code-point": "^2.0.0",
+        "strip-ansi": "^4.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        }
+      }
+    },
+    "string_decoder": {
+      "version": "0.10.31",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+      "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+    },
+    "strip-ansi": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz",
+      "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=",
+      "requires": {
+        "ansi-regex": "^0.2.1"
+      }
+    },
+    "strip-bom": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+      "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+      "dev": true,
+      "requires": {
+        "is-utf8": "^0.2.0"
+      }
+    },
+    "strip-eof": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+      "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
+      "dev": true
+    },
+    "strip-indent": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+      "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+      "dev": true,
+      "requires": {
+        "get-stdin": "^4.0.1"
+      }
+    },
+    "strip-json-comments": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+      "dev": true
+    },
+    "style-loader": {
+      "version": "0.19.0",
+      "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.19.0.tgz",
+      "integrity": "sha512-9mx9sC9nX1dgP96MZOODpGC6l1RzQBITI2D5WJhu+wnbrSYVKLGuy14XJSLVQih/0GFrPpjelt+s//VcZQ2Evw==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.0.2",
+        "schema-utils": "^0.3.0"
+      },
+      "dependencies": {
+        "schema-utils": {
+          "version": "0.3.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz",
+          "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=",
+          "dev": true,
+          "requires": {
+            "ajv": "^5.0.0"
+          }
+        }
+      }
+    },
+    "supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dev": true,
+      "requires": {
+        "has-flag": "^3.0.0"
+      }
+    },
+    "svg-baker": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/svg-baker/-/svg-baker-1.4.0.tgz",
+      "integrity": "sha512-VDI530erEOb1E8kbl2fMOCWBMnk9kz7RyjE0JtcvcAyhofSyqd0Oj7mL9MGvECexyZJn+rX6Isk6Hk21ApRcJg==",
+      "dev": true,
+      "requires": {
+        "bluebird": "^3.5.0",
+        "clone": "^2.1.1",
+        "he": "^1.1.1",
+        "image-size": "^0.5.1",
+        "loader-utils": "^1.1.0",
+        "merge-options": "1.0.1",
+        "micromatch": "3.1.0",
+        "postcss": "^5.2.17",
+        "postcss-prefix-selector": "^1.6.0",
+        "posthtml-rename-id": "^1.0",
+        "posthtml-svg-mode": "^1.0.3",
+        "query-string": "^4.3.2",
+        "traverse": "^0.6.6"
+      },
+      "dependencies": {
+        "clone": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+          "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
+          "dev": true
+        },
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          },
+          "dependencies": {
+            "kind-of": {
+              "version": "6.0.2",
+              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+              "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+              "dev": true
+            }
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          },
+          "dependencies": {
+            "kind-of": {
+              "version": "6.0.2",
+              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+              "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+              "dev": true
+            }
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          },
+          "dependencies": {
+            "kind-of": {
+              "version": "6.0.2",
+              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+              "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+              "dev": true
+            }
+          }
+        },
+        "kind-of": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+          "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+          "dev": true
+        },
+        "micromatch": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.0.tgz",
+          "integrity": "sha512-3StSelAE+hnRvMs8IdVW7Uhk8CVed5tp+kLLGlBP6WiRAXS21GPGu/Nat4WNPXj2Eoc24B02SaeoyozPMfj0/g==",
+          "dev": true,
+          "requires": {
+            "arr-diff": "^4.0.0",
+            "array-unique": "^0.3.2",
+            "braces": "^2.2.2",
+            "define-property": "^1.0.0",
+            "extend-shallow": "^2.0.1",
+            "extglob": "^2.0.2",
+            "fragment-cache": "^0.2.1",
+            "kind-of": "^5.0.2",
+            "nanomatch": "^1.2.1",
+            "object.pick": "^1.3.0",
+            "regex-not": "^1.0.0",
+            "snapdragon": "^0.8.1",
+            "to-regex": "^3.0.1"
+          }
+        }
+      }
+    },
+    "svg-baker-runtime": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/svg-baker-runtime/-/svg-baker-runtime-1.4.0.tgz",
+      "integrity": "sha512-wrv8ivS2MlkKKl9CLULEevxOSLXtCkQuwZaF9stFnvSQRp/Xexh2R5MOWpMQr+px0JMOy4I1dAXm50hq75Asrg==",
+      "dev": true,
+      "requires": {
+        "deepmerge": "1.3.2",
+        "mitt": "1.1.2",
+        "svg-baker": "^1.4.0"
+      }
+    },
+    "svg-sprite-loader": {
+      "version": "3.9.2",
+      "resolved": "https://registry.npmjs.org/svg-sprite-loader/-/svg-sprite-loader-3.9.2.tgz",
+      "integrity": "sha512-tnL7qj5ArgSYjXePzx+pZpDDzz2rMhjYdzaTjiuBz6nbPPgf2uOvO8mWj8wf/0Iv6Szd406fUYMDIyT3XnwEww==",
+      "dev": true,
+      "requires": {
+        "bluebird": "^3.5.0",
+        "deepmerge": "1.3.2",
+        "domready": "1.0.8",
+        "escape-string-regexp": "1.0.5",
+        "loader-utils": "^1.1.0",
+        "svg-baker": "^1.2.17",
+        "svg-baker-runtime": "^1.3.3",
+        "url-slug": "2.0.0"
+      }
+    },
+    "svgo": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz",
+      "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=",
+      "dev": true,
+      "requires": {
+        "coa": "~1.0.1",
+        "colors": "~1.1.2",
+        "csso": "~2.3.1",
+        "js-yaml": "~3.7.0",
+        "mkdirp": "~0.5.1",
+        "sax": "~1.2.1",
+        "whet.extend": "~0.9.9"
+      }
+    },
+    "table": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/table/-/table-5.2.2.tgz",
+      "integrity": "sha512-f8mJmuu9beQEDkKHLzOv4VxVYlU68NpdzjbGPl69i4Hx0sTopJuNxuzJd17iV2h24dAfa93u794OnDA5jqXvfQ==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.6.1",
+        "lodash": "^4.17.11",
+        "slice-ansi": "^2.0.0",
+        "string-width": "^2.1.1"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.7.0",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz",
+          "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^2.0.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "fast-deep-equal": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+          "dev": true
+        },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        }
+      }
+    },
+    "tapable": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.1.tgz",
+      "integrity": "sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA==",
+      "dev": true
+    },
+    "tar": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
+      "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=",
+      "dev": true,
+      "requires": {
+        "block-stream": "*",
+        "fstream": "^1.0.2",
+        "inherits": "2"
+      }
+    },
+    "teamcity-service-messages": {
+      "version": "0.1.9",
+      "resolved": "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.9.tgz",
+      "integrity": "sha512-agmBUllpL8n02cG/6s16St5yXYEdynkyyGDWM5qsFq9sKEkc+gBAJgcgJQCVsqlxbZZUToRwTI1hLataRjCGcw==",
+      "dev": true
+    },
+    "terser": {
+      "version": "3.14.1",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-3.14.1.tgz",
+      "integrity": "sha512-NSo3E99QDbYSMeJaEk9YW2lTg3qS9V0aKGlb+PlOrei1X02r1wSBHCNX/O+yeTRFSWPKPIGj6MqvvdqV4rnVGw==",
+      "dev": true,
+      "requires": {
+        "commander": "~2.17.1",
+        "source-map": "~0.6.1",
+        "source-map-support": "~0.5.6"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "text-encoding": {
+      "version": "0.6.4",
+      "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
+      "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=",
+      "dev": true
+    },
+    "text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+      "dev": true
+    },
+    "tf-metatags": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/tf-metatags/-/tf-metatags-2.0.0.tgz",
+      "integrity": "sha1-NNHSE4hM4iKkil0F/9Nf7mrT6Yg="
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+      "dev": true
+    },
+    "through2": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+      "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+      "dev": true,
+      "requires": {
+        "readable-stream": "~2.3.6",
+        "xtend": "~4.0.1"
+      },
+      "dependencies": {
+        "process-nextick-args": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+          "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        }
+      }
+    },
+    "thunky": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.3.tgz",
+      "integrity": "sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow==",
+      "dev": true
+    },
+    "tmp": {
+      "version": "0.0.33",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+      "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+      "dev": true,
+      "requires": {
+        "os-tmpdir": "~1.0.2"
+      }
+    },
+    "to-array": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+      "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
+    },
+    "to-arraybuffer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
+      "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=",
+      "dev": true
+    },
+    "to-fast-properties": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+      "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
+    },
+    "to-object-path": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+      "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "to-regex": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+      "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+      "dev": true,
+      "requires": {
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "regex-not": "^1.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "to-regex-range": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+      "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+      "dev": true,
+      "requires": {
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1"
+      }
+    },
+    "token-stream": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz",
+      "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=",
+      "dev": true
+    },
+    "toposort": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz",
+      "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=",
+      "dev": true
+    },
+    "tough-cookie": {
+      "version": "2.4.3",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+      "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+      "dev": true,
+      "requires": {
+        "psl": "^1.1.24",
+        "punycode": "^1.4.1"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+          "dev": true
+        }
+      }
+    },
+    "traverse": {
+      "version": "0.6.6",
+      "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz",
+      "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=",
+      "dev": true
+    },
+    "trim-newlines": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+      "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
+      "dev": true
+    },
+    "trim-right": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
+      "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM="
+    },
+    "true-case-path": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz",
+      "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.2"
+      }
+    },
+    "tslib": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
+      "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
+    },
+    "tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+      "dev": true
+    },
+    "type-check": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+      "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2"
+      }
+    },
+    "type-detect": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+      "dev": true
+    },
+    "type-is": {
+      "version": "1.6.16",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
+      "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
+      "dev": true,
+      "requires": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.18"
+      }
+    },
+    "typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
+      "dev": true
+    },
+    "typescript": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.4.tgz",
+      "integrity": "sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==",
+      "dev": true
+    },
+    "uglify-js": {
+      "version": "3.4.9",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
+      "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==",
+      "dev": true,
+      "requires": {
+        "commander": "~2.17.1",
+        "source-map": "~0.6.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "uglify-to-browserify": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz",
+      "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=",
+      "dev": true,
+      "optional": true
+    },
+    "uglifyjs-webpack-plugin": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz",
+      "integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==",
+      "dev": true,
+      "requires": {
+        "cacache": "^10.0.4",
+        "find-cache-dir": "^1.0.0",
+        "schema-utils": "^0.4.5",
+        "serialize-javascript": "^1.4.0",
+        "source-map": "^0.6.1",
+        "uglify-es": "^3.3.4",
+        "webpack-sources": "^1.1.0",
+        "worker-farm": "^1.5.2"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.13.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
+          "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        },
+        "uglify-es": {
+          "version": "3.3.9",
+          "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
+          "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==",
+          "dev": true,
+          "requires": {
+            "commander": "~2.13.0",
+            "source-map": "~0.6.1"
+          }
+        }
+      }
+    },
+    "ultron": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
+      "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
+    },
+    "unicode-canonical-property-names-ecmascript": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
+      "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==",
+      "dev": true
+    },
+    "unicode-match-property-ecmascript": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz",
+      "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==",
+      "dev": true,
+      "requires": {
+        "unicode-canonical-property-names-ecmascript": "^1.0.4",
+        "unicode-property-aliases-ecmascript": "^1.0.4"
+      }
+    },
+    "unicode-match-property-value-ecmascript": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz",
+      "integrity": "sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==",
+      "dev": true
+    },
+    "unicode-property-aliases-ecmascript": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz",
+      "integrity": "sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==",
+      "dev": true
+    },
+    "unidecode": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/unidecode/-/unidecode-0.1.8.tgz",
+      "integrity": "sha1-77swFTi8RSRqmsjFWdcvAVMFBT4=",
+      "dev": true
+    },
+    "union-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
+      "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+      "dev": true,
+      "requires": {
+        "arr-union": "^3.1.0",
+        "get-value": "^2.0.6",
+        "is-extendable": "^0.1.1",
+        "set-value": "^0.4.3"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "set-value": {
+          "version": "0.4.3",
+          "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
+          "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
+          "dev": true,
+          "requires": {
+            "extend-shallow": "^2.0.1",
+            "is-extendable": "^0.1.1",
+            "is-plain-object": "^2.0.1",
+            "to-object-path": "^0.3.0"
+          }
+        }
+      }
+    },
+    "uniq": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
+      "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
+      "dev": true
+    },
+    "uniqs": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz",
+      "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=",
+      "dev": true
+    },
+    "unique-filename": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
+      "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==",
+      "dev": true,
+      "requires": {
+        "unique-slug": "^2.0.0"
+      }
+    },
+    "unique-slug": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.1.tgz",
+      "integrity": "sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg==",
+      "dev": true,
+      "requires": {
+        "imurmurhash": "^0.1.4"
+      }
+    },
+    "unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
+      "dev": true
+    },
+    "unquote": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz",
+      "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=",
+      "dev": true
+    },
+    "unset-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+      "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+      "dev": true,
+      "requires": {
+        "has-value": "^0.3.1",
+        "isobject": "^3.0.0"
+      },
+      "dependencies": {
+        "has-value": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+          "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+          "dev": true,
+          "requires": {
+            "get-value": "^2.0.3",
+            "has-values": "^0.1.4",
+            "isobject": "^2.0.0"
+          },
+          "dependencies": {
+            "isobject": {
+              "version": "2.1.0",
+              "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+              "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+              "dev": true,
+              "requires": {
+                "isarray": "1.0.0"
+              }
+            }
+          }
+        },
+        "has-values": {
+          "version": "0.1.4",
+          "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+          "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
+          "dev": true
+        }
+      }
+    },
+    "upath": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz",
+      "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==",
+      "dev": true
+    },
+    "upper-case": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
+      "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
+      "dev": true
+    },
+    "uri-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+      "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+      "dev": true,
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "urix": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+      "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+      "dev": true
+    },
+    "url": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+      "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+      "dev": true,
+      "requires": {
+        "punycode": "1.3.2",
+        "querystring": "0.2.0"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+          "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
+          "dev": true
+        }
+      }
+    },
+    "url-parse": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.4.tgz",
+      "integrity": "sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg==",
+      "dev": true,
+      "requires": {
+        "querystringify": "^2.0.0",
+        "requires-port": "^1.0.0"
+      }
+    },
+    "url-slug": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/url-slug/-/url-slug-2.0.0.tgz",
+      "integrity": "sha1-p4nVrtSZXA2VrzM3etHVxo1NcCc=",
+      "dev": true,
+      "requires": {
+        "unidecode": "0.1.8"
+      }
+    },
+    "use": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+      "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
+      "dev": true
+    },
+    "useragent": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz",
+      "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==",
+      "dev": true,
+      "requires": {
+        "lru-cache": "4.1.x",
+        "tmp": "0.0.x"
+      }
+    },
+    "util": {
+      "version": "0.10.4",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+      "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3"
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+    },
+    "util.promisify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz",
+      "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "object.getownpropertydescriptors": "^2.0.3"
+      }
+    },
+    "utila": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
+      "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=",
+      "dev": true
+    },
+    "utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+      "dev": true
+    },
+    "uuid": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+      "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
+      "dev": true
+    },
+    "v8-compile-cache": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz",
+      "integrity": "sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw==",
+      "dev": true
+    },
+    "validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "requires": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+      "dev": true
+    },
+    "vendors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.2.tgz",
+      "integrity": "sha512-w/hry/368nO21AN9QljsaIhb9ZiZtZARoVH5f3CsFbawdLdayCgKRPup7CggujvySMxx0I91NOyxdVENohprLQ==",
+      "dev": true
+    },
+    "verror": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "^1.2.0"
+      }
+    },
+    "vm-browserify": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
+      "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=",
+      "dev": true,
+      "requires": {
+        "indexof": "0.0.1"
+      }
+    },
+    "void-elements": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
+      "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
+      "dev": true
+    },
+    "watchpack": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
+      "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==",
+      "dev": true,
+      "requires": {
+        "chokidar": "^2.0.2",
+        "graceful-fs": "^4.1.2",
+        "neo-async": "^2.5.0"
+      },
+      "dependencies": {
+        "anymatch": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+          "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+          "dev": true,
+          "requires": {
+            "micromatch": "^3.1.4",
+            "normalize-path": "^2.1.1"
+          }
+        },
+        "chokidar": {
+          "version": "2.0.4",
+          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
+          "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==",
+          "dev": true,
+          "requires": {
+            "anymatch": "^2.0.0",
+            "async-each": "^1.0.0",
+            "braces": "^2.3.0",
+            "fsevents": "^1.2.2",
+            "glob-parent": "^3.1.0",
+            "inherits": "^2.0.1",
+            "is-binary-path": "^1.0.0",
+            "is-glob": "^4.0.0",
+            "lodash.debounce": "^4.0.8",
+            "normalize-path": "^2.1.1",
+            "path-is-absolute": "^1.0.0",
+            "readdirp": "^2.0.0",
+            "upath": "^1.0.5"
+          }
+        }
+      }
+    },
+    "wbuf": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz",
+      "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==",
+      "dev": true,
+      "requires": {
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
+    "webpack": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.19.1.tgz",
+      "integrity": "sha512-j7Q/5QqZRqIFXJvC0E59ipLV5Hf6lAnS3ezC3I4HMUybwEDikQBVad5d+IpPtmaQPQArvgUZLXIN6lWijHBn4g==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.7.6",
+        "@webassemblyjs/helper-module-context": "1.7.6",
+        "@webassemblyjs/wasm-edit": "1.7.6",
+        "@webassemblyjs/wasm-parser": "1.7.6",
+        "acorn": "^5.6.2",
+        "acorn-dynamic-import": "^3.0.0",
+        "ajv": "^6.1.0",
+        "ajv-keywords": "^3.1.0",
+        "chrome-trace-event": "^1.0.0",
+        "enhanced-resolve": "^4.1.0",
+        "eslint-scope": "^4.0.0",
+        "json-parse-better-errors": "^1.0.2",
+        "loader-runner": "^2.3.0",
+        "loader-utils": "^1.1.0",
+        "memory-fs": "~0.4.1",
+        "micromatch": "^3.1.8",
+        "mkdirp": "~0.5.0",
+        "neo-async": "^2.5.0",
+        "node-libs-browser": "^2.0.0",
+        "schema-utils": "^0.4.4",
+        "tapable": "^1.1.0",
+        "uglifyjs-webpack-plugin": "^1.2.4",
+        "watchpack": "^1.5.0",
+        "webpack-sources": "^1.2.0"
+      },
+      "dependencies": {
+        "acorn-dynamic-import": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz",
+          "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==",
+          "dev": true,
+          "requires": {
+            "acorn": "^5.0.0"
+          }
+        },
+        "ajv": {
+          "version": "6.6.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz",
+          "integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^2.0.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "fast-deep-equal": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+          "dev": true
+        },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        }
+      }
+    },
+    "webpack-cli": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.1.0.tgz",
+      "integrity": "sha512-p5NeKDtYwjZozUWq6kGNs9w+Gtw/CPvyuXjXn2HMdz8Tie+krjEg8oAtonvIyITZdvpF7XG9xDHwscLr2c+ugQ==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.1",
+        "cross-spawn": "^6.0.5",
+        "enhanced-resolve": "^4.0.0",
+        "global-modules-path": "^2.1.0",
+        "import-local": "^1.0.0",
+        "inquirer": "^6.0.0",
+        "interpret": "^1.1.0",
+        "loader-utils": "^1.1.0",
+        "supports-color": "^5.4.0",
+        "v8-compile-cache": "^2.0.0",
+        "yargs": "^12.0.1"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz",
+          "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w=="
+        },
+        "camelcase": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz",
+          "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==",
+          "dev": true
+        },
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "cliui": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
+          "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
+          "dev": true,
+          "requires": {
+            "string-width": "^2.1.1",
+            "strip-ansi": "^4.0.0",
+            "wrap-ansi": "^2.0.0"
+          },
+          "dependencies": {
+            "ansi-regex": {
+              "version": "3.0.0",
+              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+              "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+              "dev": true
+            },
+            "strip-ansi": {
+              "version": "4.0.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+              "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+              "dev": true,
+              "requires": {
+                "ansi-regex": "^3.0.0"
+              }
+            }
+          }
+        },
+        "cross-spawn": {
+          "version": "6.0.5",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+          "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+          "dev": true,
+          "requires": {
+            "nice-try": "^1.0.4",
+            "path-key": "^2.0.1",
+            "semver": "^5.5.0",
+            "shebang-command": "^1.2.0",
+            "which": "^1.2.9"
+          }
+        },
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "invert-kv": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
+          "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
+          "dev": true
+        },
+        "lcid": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
+          "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
+          "dev": true,
+          "requires": {
+            "invert-kv": "^2.0.0"
+          }
+        },
+        "locate-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^3.0.0",
+            "path-exists": "^3.0.0"
+          }
+        },
+        "os-locale": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
+          "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
+          "dev": true,
+          "requires": {
+            "execa": "^1.0.0",
+            "lcid": "^2.0.0",
+            "mem": "^4.0.0"
+          }
+        },
+        "p-limit": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz",
+          "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==",
+          "dev": true,
+          "requires": {
+            "p-try": "^2.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.0.0"
+          }
+        },
+        "p-try": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz",
+          "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz",
+          "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==",
+          "requires": {
+            "ansi-regex": "^4.0.0"
+          }
+        },
+        "which-module": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+          "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+          "dev": true
+        },
+        "yargs": {
+          "version": "12.0.5",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
+          "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
+          "dev": true,
+          "requires": {
+            "cliui": "^4.0.0",
+            "decamelize": "^1.2.0",
+            "find-up": "^3.0.0",
+            "get-caller-file": "^1.0.1",
+            "os-locale": "^3.0.0",
+            "require-directory": "^2.1.1",
+            "require-main-filename": "^1.0.1",
+            "set-blocking": "^2.0.0",
+            "string-width": "^2.0.0",
+            "which-module": "^2.0.0",
+            "y18n": "^3.2.1 || ^4.0.0",
+            "yargs-parser": "^11.1.1"
+          }
+        },
+        "yargs-parser": {
+          "version": "11.1.1",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
+          "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
+          "dev": true,
+          "requires": {
+            "camelcase": "^5.0.0",
+            "decamelize": "^1.2.0"
+          }
+        }
+      }
+    },
+    "webpack-dev-middleware": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.5.0.tgz",
+      "integrity": "sha512-1Zie7+dMr4Vv3nGyhr8mxGQkzTQK1PTS8K3yJ4yB1mfRGwO1DzQibgmNfUqbEfQY6eEtEEUzC+o7vhpm/Sfn5w==",
+      "dev": true,
+      "requires": {
+        "memory-fs": "~0.4.1",
+        "mime": "^2.3.1",
+        "range-parser": "^1.0.3",
+        "webpack-log": "^2.0.0"
+      },
+      "dependencies": {
+        "mime": {
+          "version": "2.4.0",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz",
+          "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==",
+          "dev": true
+        }
+      }
+    },
+    "webpack-dev-server": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.2.0.tgz",
+      "integrity": "sha512-CUGPLQsUBVKa/qkZl1MMo8krm30bsOHAP8jtn78gUICpT+sR3esN4Zb0TSBzOEEQJF0zHNEbwx5GHInkqcmlsA==",
+      "dev": true,
+      "requires": {
+        "ansi-html": "0.0.7",
+        "bonjour": "^3.5.0",
+        "chokidar": "^2.0.0",
+        "compression": "^1.5.2",
+        "connect-history-api-fallback": "^1.3.0",
+        "debug": "^4.1.1",
+        "del": "^3.0.0",
+        "express": "^4.16.2",
+        "html-entities": "^1.2.0",
+        "http-proxy-middleware": "^0.19.1",
+        "import-local": "^2.0.0",
+        "internal-ip": "^4.0.0",
+        "ip": "^1.1.5",
+        "killable": "^1.0.0",
+        "loglevel": "^1.4.1",
+        "opn": "^5.1.0",
+        "portfinder": "^1.0.9",
+        "schema-utils": "^1.0.0",
+        "selfsigned": "^1.9.1",
+        "semver": "^5.6.0",
+        "serve-index": "^1.7.2",
+        "sockjs": "0.3.19",
+        "sockjs-client": "1.3.0",
+        "spdy": "^4.0.0",
+        "strip-ansi": "^3.0.0",
+        "supports-color": "^6.1.0",
+        "url": "^0.11.0",
+        "webpack-dev-middleware": "^3.5.1",
+        "webpack-log": "^2.0.0",
+        "yargs": "12.0.2"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.9.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.2.tgz",
+          "integrity": "sha512-4UFy0/LgDo7Oa/+wOAlj44tp9K78u38E5/359eSrqEp1Z5PdVfimCcs7SluXMP755RUQu6d2b4AvF0R1C9RZjg==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^2.0.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "camelcase": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+          "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
+          "dev": true
+        },
+        "cliui": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
+          "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
+          "dev": true,
+          "requires": {
+            "string-width": "^2.1.1",
+            "strip-ansi": "^4.0.0",
+            "wrap-ansi": "^2.0.0"
+          },
+          "dependencies": {
+            "ansi-regex": {
+              "version": "3.0.0",
+              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+              "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+              "dev": true
+            },
+            "strip-ansi": {
+              "version": "4.0.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+              "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+              "dev": true,
+              "requires": {
+                "ansi-regex": "^3.0.0"
+              }
+            }
+          }
+        },
+        "decamelize": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz",
+          "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==",
+          "dev": true,
+          "requires": {
+            "xregexp": "4.0.0"
+          }
+        },
+        "fast-deep-equal": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+          "dev": true
+        },
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "import-local": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz",
+          "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==",
+          "dev": true,
+          "requires": {
+            "pkg-dir": "^3.0.0",
+            "resolve-cwd": "^2.0.0"
+          }
+        },
+        "invert-kv": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
+          "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
+          "dev": true
+        },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        },
+        "lcid": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
+          "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
+          "dev": true,
+          "requires": {
+            "invert-kv": "^2.0.0"
+          }
+        },
+        "locate-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^3.0.0",
+            "path-exists": "^3.0.0"
+          }
+        },
+        "os-locale": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
+          "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
+          "dev": true,
+          "requires": {
+            "execa": "^1.0.0",
+            "lcid": "^2.0.0",
+            "mem": "^4.0.0"
+          }
+        },
+        "p-limit": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz",
+          "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==",
+          "dev": true,
+          "requires": {
+            "p-try": "^2.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.0.0"
+          }
+        },
+        "p-try": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz",
+          "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==",
+          "dev": true
+        },
+        "pkg-dir": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
+          "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==",
+          "dev": true,
+          "requires": {
+            "find-up": "^3.0.0"
+          }
+        },
+        "schema-utils": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+          "dev": true,
+          "requires": {
+            "ajv": "^6.1.0",
+            "ajv-errors": "^1.0.0",
+            "ajv-keywords": "^3.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+          "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        },
+        "webpack-dev-middleware": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.6.0.tgz",
+          "integrity": "sha512-oeXA3m+5gbYbDBGo4SvKpAHJJEGMoekUbHgo1RK7CP1sz7/WOSeu/dWJtSTk+rzDCLkPwQhGocgIq6lQqOyOwg==",
+          "dev": true,
+          "requires": {
+            "memory-fs": "^0.4.1",
+            "mime": "^2.3.1",
+            "range-parser": "^1.0.3",
+            "webpack-log": "^2.0.0"
+          }
+        },
+        "which-module": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+          "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+          "dev": true
+        },
+        "yargs": {
+          "version": "12.0.2",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.2.tgz",
+          "integrity": "sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ==",
+          "dev": true,
+          "requires": {
+            "cliui": "^4.0.0",
+            "decamelize": "^2.0.0",
+            "find-up": "^3.0.0",
+            "get-caller-file": "^1.0.1",
+            "os-locale": "^3.0.0",
+            "require-directory": "^2.1.1",
+            "require-main-filename": "^1.0.1",
+            "set-blocking": "^2.0.0",
+            "string-width": "^2.0.0",
+            "which-module": "^2.0.0",
+            "y18n": "^3.2.1 || ^4.0.0",
+            "yargs-parser": "^10.1.0"
+          }
+        },
+        "yargs-parser": {
+          "version": "10.1.0",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",
+          "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==",
+          "dev": true,
+          "requires": {
+            "camelcase": "^4.1.0"
+          }
+        }
+      }
+    },
+    "webpack-log": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz",
+      "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==",
+      "dev": true,
+      "requires": {
+        "ansi-colors": "^3.0.0",
+        "uuid": "^3.3.2"
+      }
+    },
+    "webpack-merge": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.4.tgz",
+      "integrity": "sha512-TmSe1HZKeOPey3oy1Ov2iS3guIZjWvMT2BBJDzzT5jScHTjVC3mpjJofgueEzaEd6ibhxRDD6MIblDr8tzh8iQ==",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.17.5"
+      }
+    },
+    "webpack-sources": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",
+      "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==",
+      "dev": true,
+      "requires": {
+        "source-list-map": "^2.0.0",
+        "source-map": "~0.6.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "websocket-driver": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz",
+      "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=",
+      "dev": true,
+      "requires": {
+        "http-parser-js": ">=0.4.0",
+        "websocket-extensions": ">=0.1.1"
+      }
+    },
+    "websocket-extensions": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
+      "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
+      "dev": true
+    },
+    "whet.extend": {
+      "version": "0.9.9",
+      "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz",
+      "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=",
+      "dev": true
+    },
+    "which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "dev": true,
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "which-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
+      "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
+      "dev": true
+    },
+    "wide-align": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+      "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+      "dev": true,
+      "requires": {
+        "string-width": "^1.0.2 || 2"
+      }
+    },
+    "window-size": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
+      "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=",
+      "dev": true
+    },
+    "with": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz",
+      "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=",
+      "dev": true,
+      "requires": {
+        "acorn": "^3.1.0",
+        "acorn-globals": "^3.0.0"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "3.3.0",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
+          "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
+          "dev": true
+        }
+      }
+    },
+    "wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
+      "dev": true
+    },
+    "worker-farm": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz",
+      "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==",
+      "dev": true,
+      "requires": {
+        "errno": "~0.1.7"
+      }
+    },
+    "worker-loader": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-2.0.0.tgz",
+      "integrity": "sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.0.0",
+        "schema-utils": "^0.4.0"
+      }
+    },
+    "wrap-ansi": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+      "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+      "dev": true,
+      "requires": {
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.1"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+          "dev": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "dev": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        }
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    },
+    "write": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz",
+      "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=",
+      "dev": true,
+      "requires": {
+        "mkdirp": "^0.5.1"
+      }
+    },
+    "ws": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
+      "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
+      "requires": {
+        "async-limiter": "~1.0.0",
+        "safe-buffer": "~5.1.0",
+        "ultron": "~1.1.0"
+      }
+    },
+    "xmlhttprequest-ssl": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
+      "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
+    },
+    "xregexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz",
+      "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==",
+      "dev": true
+    },
+    "xtend": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+      "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
+      "dev": true
+    },
+    "y18n": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+      "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
+      "dev": true
+    },
+    "yallist": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+      "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+      "dev": true
+    },
+    "yargs": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-9.0.1.tgz",
+      "integrity": "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=",
+      "dev": true,
+      "requires": {
+        "camelcase": "^4.1.0",
+        "cliui": "^3.2.0",
+        "decamelize": "^1.1.1",
+        "get-caller-file": "^1.0.1",
+        "os-locale": "^2.0.0",
+        "read-pkg-up": "^2.0.0",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^1.0.1",
+        "set-blocking": "^2.0.0",
+        "string-width": "^2.0.0",
+        "which-module": "^2.0.0",
+        "y18n": "^3.2.1",
+        "yargs-parser": "^7.0.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+          "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
+          "dev": true
+        },
+        "execa": {
+          "version": "0.7.0",
+          "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz",
+          "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=",
+          "dev": true,
+          "requires": {
+            "cross-spawn": "^5.0.1",
+            "get-stream": "^3.0.0",
+            "is-stream": "^1.1.0",
+            "npm-run-path": "^2.0.0",
+            "p-finally": "^1.0.0",
+            "signal-exit": "^3.0.0",
+            "strip-eof": "^1.0.0"
+          }
+        },
+        "get-stream": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
+          "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
+          "dev": true
+        },
+        "load-json-file": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
+          "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "^4.1.2",
+            "parse-json": "^2.2.0",
+            "pify": "^2.0.0",
+            "strip-bom": "^3.0.0"
+          }
+        },
+        "mem": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
+          "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=",
+          "dev": true,
+          "requires": {
+            "mimic-fn": "^1.0.0"
+          }
+        },
+        "os-locale": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz",
+          "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==",
+          "dev": true,
+          "requires": {
+            "execa": "^0.7.0",
+            "lcid": "^1.0.0",
+            "mem": "^1.1.0"
+          }
+        },
+        "path-type": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz",
+          "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=",
+          "dev": true,
+          "requires": {
+            "pify": "^2.0.0"
+          }
+        },
+        "pify": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+          "dev": true
+        },
+        "read-pkg": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
+          "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=",
+          "dev": true,
+          "requires": {
+            "load-json-file": "^2.0.0",
+            "normalize-package-data": "^2.3.2",
+            "path-type": "^2.0.0"
+          }
+        },
+        "read-pkg-up": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz",
+          "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=",
+          "dev": true,
+          "requires": {
+            "find-up": "^2.0.0",
+            "read-pkg": "^2.0.0"
+          }
+        },
+        "strip-bom": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+          "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+          "dev": true
+        },
+        "which-module": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+          "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+          "dev": true
+        },
+        "y18n": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+          "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
+          "dev": true
+        },
+        "yargs-parser": {
+          "version": "7.0.0",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz",
+          "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=",
+          "dev": true,
+          "requires": {
+            "camelcase": "^4.1.0"
+          }
+        }
+      }
+    },
+    "yargs-parser": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
+      "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
+      "dev": true,
+      "requires": {
+        "camelcase": "^3.0.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+          "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+          "dev": true
+        }
+      }
+    },
+    "yargs-unparser": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz",
+      "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==",
+      "dev": true,
+      "requires": {
+        "flat": "^4.1.0",
+        "lodash": "^4.17.11",
+        "yargs": "^12.0.5"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "camelcase": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz",
+          "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==",
+          "dev": true
+        },
+        "cliui": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
+          "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
+          "dev": true,
+          "requires": {
+            "string-width": "^2.1.1",
+            "strip-ansi": "^4.0.0",
+            "wrap-ansi": "^2.0.0"
+          }
+        },
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "invert-kv": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
+          "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
+          "dev": true
+        },
+        "lcid": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
+          "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
+          "dev": true,
+          "requires": {
+            "invert-kv": "^2.0.0"
+          }
+        },
+        "locate-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^3.0.0",
+            "path-exists": "^3.0.0"
+          }
+        },
+        "os-locale": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
+          "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
+          "dev": true,
+          "requires": {
+            "execa": "^1.0.0",
+            "lcid": "^2.0.0",
+            "mem": "^4.0.0"
+          }
+        },
+        "p-limit": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz",
+          "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==",
+          "dev": true,
+          "requires": {
+            "p-try": "^2.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.0.0"
+          }
+        },
+        "p-try": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz",
+          "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        },
+        "which-module": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+          "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+          "dev": true
+        },
+        "yargs": {
+          "version": "12.0.5",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
+          "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
+          "dev": true,
+          "requires": {
+            "cliui": "^4.0.0",
+            "decamelize": "^1.2.0",
+            "find-up": "^3.0.0",
+            "get-caller-file": "^1.0.1",
+            "os-locale": "^3.0.0",
+            "require-directory": "^2.1.1",
+            "require-main-filename": "^1.0.1",
+            "set-blocking": "^2.0.0",
+            "string-width": "^2.0.0",
+            "which-module": "^2.0.0",
+            "y18n": "^3.2.1 || ^4.0.0",
+            "yargs-parser": "^11.1.1"
+          }
+        },
+        "yargs-parser": {
+          "version": "11.1.1",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
+          "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
+          "dev": true,
+          "requires": {
+            "camelcase": "^5.0.0",
+            "decamelize": "^1.2.0"
+          }
+        }
+      }
+    },
+    "yeast": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+      "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
+    }
+  }
+}
diff --git a/modules/frontend/package.json b/modules/frontend/package.json
new file mode 100644
index 0000000..d24adfe
--- /dev/null
+++ b/modules/frontend/package.json
@@ -0,0 +1,147 @@
+{
+  "name": "ignite-web-console",
+  "version": "2.7.0",
+  "description": "Interactive Web console for configuration, executing SQL queries and monitoring of Apache Ignite Cluster",
+  "private": true,
+  "main": "index.js",
+  "scripts": {
+    "start": "webpack-dev-server --config ./webpack/webpack.dev.js",
+    "dev": "npm start",
+    "build": "webpack --config ./webpack/webpack.prod.js",
+    "test": "karma start ./test/karma.conf.js",
+    "test-watch": "npm test -- --no-single-run"
+  },
+  "license": "Apache-2.0",
+  "keywords": [
+    "Apache Ignite Web console"
+  ],
+  "homepage": "https://ignite.apache.org/",
+  "engines": {
+    "npm": ">=5.0.0",
+    "node": ">=8.0.0 <10.0.0"
+  },
+  "os": [
+    "darwin",
+    "linux",
+    "win32"
+  ],
+  "dependencies": {
+    "@babel/plugin-transform-parameters": "7.0.0",
+    "@uirouter/angularjs": "1.0.20",
+    "@uirouter/core": "5.0.19",
+    "@uirouter/rx": "0.5.0",
+    "angular": "1.7.6",
+    "angular-acl": "0.1.10",
+    "angular-animate": "1.7.6",
+    "angular-aria": "1.7.6",
+    "angular-drag-and-drop-lists": "1.4.0",
+    "angular-gridster": "0.13.14",
+    "angular-messages": "1.7.6",
+    "angular-nvd3": "1.0.9",
+    "angular-sanitize": "1.7.6",
+    "angular-smart-table": "2.1.11",
+    "angular-strap": "2.3.12",
+    "angular-translate": "2.18.1",
+    "angular-tree-control": "0.2.28",
+    "angular-ui-grid": "4.6.1",
+    "angular-ui-validate": "1.2.3",
+    "angular1-async-filter": "1.1.0",
+    "brace": "0.11.1",
+    "browser-update": "3.1.13",
+    "bson-objectid": "1.1.5",
+    "chart.js": "2.7.2",
+    "chartjs-plugin-streaming": "1.6.1",
+    "file-saver": "1.3.3",
+    "font-awesome": "4.7.0",
+    "jquery": "3.2.1",
+    "json-bigint": "0.3.0",
+    "jsondiffpatch": "0.2.5",
+    "jszip": "3.1.5",
+    "lodash": "4.17.11",
+    "natural-compare-lite": "1.4.0",
+    "nvd3": "1.8.6",
+    "outdent": "0.5.0",
+    "pako": "1.0.6",
+    "resize-observer-polyfill": "1.5.0",
+    "roboto-font": "0.1.0",
+    "rxjs": "6.3.3",
+    "socket.io-client": "2.1.1",
+    "tf-metatags": "2.0.0"
+  },
+  "devDependencies": {
+    "@babel/core": "7.0.1",
+    "@babel/plugin-proposal-class-properties": "7.0.0",
+    "@babel/plugin-proposal-object-rest-spread": "7.0.0",
+    "@babel/plugin-syntax-dynamic-import": "7.0.0",
+    "@babel/preset-env": "7.0.0",
+    "@babel/preset-typescript": "7.1.0",
+    "@types/angular": "1.6.51",
+    "@types/angular-animate": "1.5.10",
+    "@types/angular-mocks": "1.5.12",
+    "@types/angular-strap": "2.3.1",
+    "@types/angular-translate": "2.16.0",
+    "@types/chai": "4.1.4",
+    "@types/copy-webpack-plugin": "4.4.2",
+    "@types/karma": "1.7.4",
+    "@types/lodash": "4.14.110",
+    "@types/mini-css-extract-plugin": "0.2.0",
+    "@types/mocha": "2.2.48",
+    "@types/node": "10.5.1",
+    "@types/sinon": "4.0.0",
+    "@types/socket.io-client": "1.4.32",
+    "@types/ui-grid": "0.0.38",
+    "@types/webpack": "4.4.11",
+    "@types/webpack-merge": "4.1.3",
+    "@typescript-eslint/eslint-plugin": "1.1.0",
+    "@typescript-eslint/parser": "1.1.0",
+    "angular-mocks": "1.7.6",
+    "app-root-path": "2.0.1",
+    "babel-loader": "8.0.2",
+    "bootstrap-sass": "3.3.7",
+    "chai": "4.1.0",
+    "chalk": "2.1.0",
+    "copy-webpack-plugin": "4.5.2",
+    "css-loader": "0.28.7",
+    "eslint": "5.12.1",
+    "eslint-formatter-friendly": "6.0.0",
+    "eslint-loader": "2.1.0",
+    "expose-loader": "0.7.5",
+    "file-loader": "1.1.11",
+    "glob": "7.1.2",
+    "globby": "8.0.1",
+    "html-loader": "1.0.0-alpha.0",
+    "html-webpack-plugin": "3.2.0",
+    "ignore-loader": "0.1.2",
+    "karma": "^4.0.0",
+    "karma-babel-preprocessor": "8.0.0-beta.0",
+    "karma-chrome-launcher": "2.2.0",
+    "karma-mocha": "1.3.0",
+    "karma-mocha-reporter": "2.2.3",
+    "karma-teamcity-reporter": "1.0.0",
+    "karma-webpack": "4.0.0-rc.2",
+    "mini-css-extract-plugin": "0.4.2",
+    "mocha": "^6.0.0",
+    "mocha-teamcity-reporter": "1.1.1",
+    "node-fetch": "1.7.3",
+    "node-sass": "4.10.0",
+    "progress": "2.0.0",
+    "progress-bar-webpack-plugin": "1.11.0",
+    "pug-html-loader": "1.1.0",
+    "pug-loader": "2.4.0",
+    "resolve-url-loader": "2.1.0",
+    "sass-loader": "6.0.7",
+    "sinon": "2.3.8",
+    "slash": "1.0.0",
+    "style-loader": "0.19.0",
+    "svg-sprite-loader": "3.9.2",
+    "teamcity-service-messages": "0.1.9",
+    "typescript": "3.2.4",
+    "uglifyjs-webpack-plugin": "1.3.0",
+    "webpack": "4.19.1",
+    "webpack-cli": "3.1.0",
+    "webpack-dev-server": "^3.2.0",
+    "webpack-merge": "4.1.4",
+    "worker-loader": "2.0.0",
+    "yargs": "9.0.1"
+  }
+}
diff --git a/modules/frontend/public/favicon.ico b/modules/frontend/public/favicon.ico
new file mode 100644
index 0000000..b36f8d7
--- /dev/null
+++ b/modules/frontend/public/favicon.ico
Binary files differ
diff --git a/modules/frontend/public/images/cache.png b/modules/frontend/public/images/cache.png
new file mode 100644
index 0000000..2fb3bc8
--- /dev/null
+++ b/modules/frontend/public/images/cache.png
Binary files differ
diff --git a/modules/frontend/public/images/checkbox-active.svg b/modules/frontend/public/images/checkbox-active.svg
new file mode 100644
index 0000000..47c4d88
--- /dev/null
+++ b/modules/frontend/public/images/checkbox-active.svg
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: sketchtool 45.2 (43514) - http://www.bohemiancoding.com/sketch -->
+    <title>1F50951A-D0DF-4DE9-B464-5A57049A9426</title>
+    <desc>Created with sketchtool.</desc>
+    <defs>
+        <polygon id="path-1" points="4.0575 5.993 5.0835 7.0255 8.99916406 3.06347168 10.0556641 4.12097168 5.0885 9.1435 3 7.0505"></polygon>
+        <filter x="-28.3%" y="-16.4%" width="156.7%" height="165.8%" filterUnits="objectBoundingBox" id="filter-2">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0.029487408   0 0 0 0 0.138965552   0 0 0 0 0.210990646  0 0 0 0.5 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Checkbox" transform="translate(-51.000000, -254.000000)">
+            <g id="CheckboxActive" transform="translate(51.000000, 254.000000)">
+                <rect id="Rectangle-2-Copy" fill="#0065B6" x="0" y="0" width="12" height="12" rx="2"></rect>
+                <g id="Imported-Layers">
+                    <use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
+                    <use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/modules/frontend/public/images/checkbox.svg b/modules/frontend/public/images/checkbox.svg
new file mode 100644
index 0000000..82264a9
--- /dev/null
+++ b/modules/frontend/public/images/checkbox.svg
@@ -0,0 +1,22 @@
+<svg version="1.1" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+  <linearGradient id="linearGradient-1" x1="50%" x2="50%" y1="-18.787%" y2="68.083%">
+   <stop stop-color="#979797" offset="0"/>
+   <stop stop-color="#BEBEBE" offset="1"/>
+  </linearGradient>
+  <rect id="path-2" x="148" y="254" width="12" height="12" rx="2"/>
+  <filter id="filter-3" x="-12.5%" y="-12.5%" width="125%" height="125%">
+   <feGaussianBlur in="SourceAlpha" result="shadowBlurInner1" stdDeviation="1"/>
+   <feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"/>
+   <feComposite in="shadowOffsetInner1" in2="SourceAlpha" k2="-1" k3="1" operator="arithmetic" result="shadowInnerInner1"/>
+   <feColorMatrix in="shadowInnerInner1" values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.5 0"/>
+  </filter>
+ </defs>
+ <g fill="none" fill-rule="evenodd">
+  <g id="Checkbox" transform="translate(-148 -254)">
+   <use width="100%" height="100%" fill="#ffffff" xlink:href="#path-2"/>
+   <use fill="black" filter="url(#filter-3)" xlink:href="#path-2"/>
+   <rect x="148.5" y="254.5" width="11" height="11" rx="2" stroke="url(#linearGradient-1)"/>
+  </g>
+ </g>
+</svg>
diff --git a/modules/frontend/public/images/cluster-quick.png b/modules/frontend/public/images/cluster-quick.png
new file mode 100644
index 0000000..9f3933e
--- /dev/null
+++ b/modules/frontend/public/images/cluster-quick.png
Binary files differ
diff --git a/modules/frontend/public/images/cluster.png b/modules/frontend/public/images/cluster.png
new file mode 100644
index 0000000..727f0c1
--- /dev/null
+++ b/modules/frontend/public/images/cluster.png
Binary files differ
diff --git a/modules/frontend/public/images/collapse.svg b/modules/frontend/public/images/collapse.svg
new file mode 100644
index 0000000..86861a5
--- /dev/null
+++ b/modules/frontend/public/images/collapse.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg">
+ <path d="m2 0c-1.108 0-2 0.892-2 2v9c0 1.108 0.892 2 2 2h9c1.108 0 2-0.892 2-2v-9c0-1.108-0.892-2-2-2h-9zm1 6h7v1h-7v-1z" fill="#757575"/>
+</svg>
diff --git a/modules/frontend/public/images/domains.png b/modules/frontend/public/images/domains.png
new file mode 100644
index 0000000..f22b04d
--- /dev/null
+++ b/modules/frontend/public/images/domains.png
Binary files differ
diff --git a/modules/frontend/public/images/expand.svg b/modules/frontend/public/images/expand.svg
new file mode 100644
index 0000000..569c9c0
--- /dev/null
+++ b/modules/frontend/public/images/expand.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg">
+ <path d="m2 0c-1.108 0-2 0.892-2 2v9c0 1.108 0.892 2 2 2h9c1.108 0 2-0.892 2-2v-9c0-1.108-0.892-2-2-2zm4 3h1v3h3v1h-3v3h-1v-3h-3v-1h3z" fill="#757575" />
+</svg>
diff --git a/modules/frontend/public/images/icons/alert.icon.svg b/modules/frontend/public/images/icons/alert.icon.svg
new file mode 100644
index 0000000..6d1b3e2
--- /dev/null
+++ b/modules/frontend/public/images/icons/alert.icon.svg
@@ -0,0 +1 @@
+<svg id="alert-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 19"><path fill="currentColor" d="M8 18.7c1 0 1.8-.8 1.8-1.8H6.2c0 1 .8 1.8 1.8 1.8zM14.1 8c0-2.9-2-5.3-4.7-5.9v-.7C9.4.6 8.8 0 8 0S6.6.6 6.6 1.4V2c-2.7.7-4.7 3.1-4.7 6v5.2L0 15.1v.9h16v-.9l-1.9-1.9V8z"/></svg>
diff --git a/modules/frontend/public/images/icons/attention.icon.svg b/modules/frontend/public/images/icons/attention.icon.svg
new file mode 100644
index 0000000..cd8a3a1
--- /dev/null
+++ b/modules/frontend/public/images/icons/attention.icon.svg
@@ -0,0 +1,3 @@
+<svg id='attention-icon' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14">
+    <path fill="currentColor" fill-rule="evenodd" d="M6.3 9.1h1.4v1.4H6.3V9.1zm0-5.6h1.4v4.2H6.3V3.5zM6.993 0A6.997 6.997 0 0 0 0 7c0 3.864 3.129 7 6.993 7A7.004 7.004 0 0 0 14 7c0-3.864-3.136-7-7.007-7zM7 12.6A5.598 5.598 0 0 1 1.4 7c0-3.094 2.506-5.6 5.6-5.6s5.6 2.506 5.6 5.6-2.506 5.6-5.6 5.6z"/>
+</svg>
diff --git a/modules/frontend/public/images/icons/check.icon.svg b/modules/frontend/public/images/icons/check.icon.svg
new file mode 100644
index 0000000..ed74764
--- /dev/null
+++ b/modules/frontend/public/images/icons/check.icon.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="11" viewBox="0 0 14 11">
+    <path fill="#FFF" fill-rule="evenodd" d="M4.45 8.42L1.13 5.103 0 6.224l4.45 4.45L14 1.121 12.878 0 4.449 8.42"/>
+</svg>
diff --git a/modules/frontend/public/images/icons/checkmark.icon.svg b/modules/frontend/public/images/icons/checkmark.icon.svg
new file mode 100644
index 0000000..74cacf6
--- /dev/null
+++ b/modules/frontend/public/images/icons/checkmark.icon.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" viewBox="0 0 14 11" xmlns="http://www.w3.org/2000/svg">
+    <path fill="currentColor" d="m12.877 0.32617l-8.4277 8.4219-3.3184-3.3203-1.1309 1.123 4.4492 4.4492 9.5508-9.5508-1.123-1.123z"/>
+</svg>
diff --git a/modules/frontend/public/images/icons/clock.icon.svg b/modules/frontend/public/images/icons/clock.icon.svg
new file mode 100644
index 0000000..63414d3
--- /dev/null
+++ b/modules/frontend/public/images/icons/clock.icon.svg
@@ -0,0 +1 @@
+<svg id='clock-icon' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M6.8 10.1L5.1 8.4l-.8.9 2.5 2.5L11.6 7l-.8-.8-4 3.9zM8 14.5c-3.1 0-5.6-2.5-5.6-5.6 0-3.1 2.5-5.6 5.6-5.6s5.6 2.5 5.6 5.6c0 3.1-2.5 5.6-5.6 5.6zM8 1.7C4 1.7.8 4.9.8 8.9S4 16.1 8 16.1s7.2-3.2 7.2-7.2S12 1.7 8 1.7zm-3.3-.5L3.7 0 0 3.1l1 1.2 3.7-3.1zM16 3.1L12.3 0l-1 1.2L15 4.3l1-1.2z"/></svg>
\ No newline at end of file
diff --git a/modules/frontend/public/images/icons/cluster.icon.svg b/modules/frontend/public/images/icons/cluster.icon.svg
new file mode 100644
index 0000000..7576049
--- /dev/null
+++ b/modules/frontend/public/images/icons/cluster.icon.svg
@@ -0,0 +1,10 @@
+<svg version="1.1" viewBox="0 0 20 17" xmlns="http://www.w3.org/2000/svg">
+	<g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-width="2">
+		<rect x="8.5" y="1" width="3" height="3" rx="1"/>
+		<rect x="8.5" y="12.25" width="3" height="3" rx="1"/>
+		<rect x="16" y="12.25" width="3" height="3" rx="1"/>
+		<rect x="1" y="12.25" width="3" height="3" rx="1"/>
+		<path d="M10 5v2.5l7.656.05v4.206" stroke-linecap="square" stroke-linejoin="round"/>
+		<path d="M10.156 5v6.756V7.5L2.5 7.55v4.206" stroke-linecap="round" stroke-linejoin="round"/>
+	</g>
+</svg>
diff --git a/modules/frontend/public/images/icons/collapse.icon.svg b/modules/frontend/public/images/icons/collapse.icon.svg
new file mode 100644
index 0000000..eb16b4c
--- /dev/null
+++ b/modules/frontend/public/images/icons/collapse.icon.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg">
+ <path d="m2 0c-1.108 0-2 0.892-2 2v9c0 1.108 0.892 2 2 2h9c1.108 0 2-0.892 2-2v-9c0-1.108-0.892-2-2-2h-9zm1 6h7v1h-7v-1z" fill="currentColor"/>
+</svg>
diff --git a/modules/frontend/public/images/icons/copy.icon.svg b/modules/frontend/public/images/icons/copy.icon.svg
new file mode 100644
index 0000000..b04d4ea
--- /dev/null
+++ b/modules/frontend/public/images/icons/copy.icon.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" viewBox="0 0 16 19" xmlns="http://www.w3.org/2000/svg">
+ <path class="st0" d="m11.8 0h-10.1c-0.9 0-1.7 0.8-1.7 1.7v11.8h1.7v-11.8h10.1v-1.7zm2.5 3.4h-9.3c-0.9 0-1.7 0.8-1.7 1.7v11.8c0 0.9 0.8 1.7 1.7 1.7h9.3c0.9 0 1.7-0.8 1.7-1.7v-11.8c0-1-0.8-1.7-1.7-1.7zm-9.2 1.7h9.3v11.8h-9.3z" fill="currentColor"/>
+</svg>
diff --git a/modules/frontend/public/images/icons/cross.icon.svg b/modules/frontend/public/images/icons/cross.icon.svg
new file mode 100644
index 0000000..5fa950d
--- /dev/null
+++ b/modules/frontend/public/images/icons/cross.icon.svg
@@ -0,0 +1 @@
+<svg id="cross-icon" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M10.791 0L6 4.791 1.209 0 0 1.209 4.791 6 0 10.791 1.209 12 6 7.209 10.791 12 12 10.791 7.209 6 12 1.209z" fill-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/modules/frontend/public/images/icons/csv.icon.svg b/modules/frontend/public/images/icons/csv.icon.svg
new file mode 100644
index 0000000..b817b7b
--- /dev/null
+++ b/modules/frontend/public/images/icons/csv.icon.svg
@@ -0,0 +1 @@
+<svg id="csv-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M10.4 61.6v380.8l280.1 49.8V11.8L10.4 61.6zm161 270.5l-23.5-61.7-23.1 58.5H92.7l37.5-81.6-34.8-80h33l21.3 55.2 25.3-59.9 31.7-1.6-39.6 85.5 41.2 88.3-36.9-2.7zM489.3 61.1H300v27.8h71.2V139H300v15.1h71.2v50.1H300v15.1h71.2v50.1H300v15.1h71.2v50.2H300v15.4h71.2v50.1H300v32.2h189.3c5.4 0 9.7-4.5 9.7-10V71.2c0-5.6-4.4-10.1-9.7-10.1zm-23.1 339.2h-80.3v-50.1h80.3v50.1zm0-65.5h-80.3v-50.2h80.3v50.2zm0-65.2h-80.3v-50.1h80.3v50.1zm0-65.3h-80.3v-50.2h80.3v50.2zm0-65.2h-80.3V88.9h80.3v50.2z"/></svg>
\ No newline at end of file
diff --git a/modules/frontend/public/images/icons/download.icon.svg b/modules/frontend/public/images/icons/download.icon.svg
new file mode 100644
index 0000000..0693c87
--- /dev/null
+++ b/modules/frontend/public/images/icons/download.icon.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="download-icon" version="1.1" viewBox="0 0 12 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect y="14.5" width="12" height="1.5" fill="currentColor"/><polygon points="5.2 9.1 1.1 4.9 0 6 6 12 12 6 10.9 4.9 6.8 9.1 6.8 0 5.2 0" fill="currentColor"/></svg>
diff --git a/modules/frontend/public/images/icons/downloadAgent.icon.svg b/modules/frontend/public/images/icons/downloadAgent.icon.svg
new file mode 100644
index 0000000..afb51ee
--- /dev/null
+++ b/modules/frontend/public/images/icons/downloadAgent.icon.svg
@@ -0,0 +1,16 @@
+<svg version="1.1" viewBox="0 0 20 18" xmlns="http://www.w3.org/2000/svg">
+	<g fill="none" fill-rule="evenodd">
+		<g transform="translate(-263 -1597)" fill="currentColor">
+			<g transform="translate(235 1485)">
+				<g transform="translate(28 112)">
+					<path d="m-8.7041e-14 9.0435v7.1788c0 0.97787 0.81811 1.7778 1.8182 1.7778h16.364c0.99966 0 1.8178-0.79991 1.8178-1.7778v-7.1788h-20zm1.7103 0h16.472v7.1876h-16.364l-0.10794-7.1876z"/>
+					<g stroke="currentColor" stroke-width=".5">
+						<polygon points="14.7 8.635 14.131 8.0659 10.757 11.44 10.757 5 9.9435 5 9.9435 11.44 6.5692 8.0659 6 8.635 10.35 12.985"/>
+						<polygon points="10.757 3.719 10.757 3 9.9435 3 9.9435 3.719"/>
+						<polygon points="10.757 1.719 10.757 1 9.9435 1 9.9435 1.719"/>
+					</g>
+				</g>
+			</g>
+		</g>
+	</g>
+</svg>
diff --git a/modules/frontend/public/images/icons/exclamation.icon.svg b/modules/frontend/public/images/icons/exclamation.icon.svg
new file mode 100644
index 0000000..95e4613
--- /dev/null
+++ b/modules/frontend/public/images/icons/exclamation.icon.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
+    <path fill="#F34718" fill-rule="evenodd" d="M6.3 9.1h1.4v1.4H6.3V9.1zm0-5.6h1.4v4.2H6.3V3.5zM6.993 0A6.997 6.997 0 0 0 0 7c0 3.864 3.129 7 6.993 7A7.004 7.004 0 0 0 14 7c0-3.864-3.136-7-7.007-7zM7 12.6A5.598 5.598 0 0 1 1.4 7c0-3.094 2.506-5.6 5.6-5.6s5.6 2.506 5.6 5.6-2.506 5.6-5.6 5.6z"/>
+</svg>
diff --git a/modules/frontend/public/images/icons/exit.icon.svg b/modules/frontend/public/images/icons/exit.icon.svg
new file mode 100644
index 0000000..a355dcd
--- /dev/null
+++ b/modules/frontend/public/images/icons/exit.icon.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="18" viewBox="0 0 16 18">
+    <path fill="currentColor" fill-rule="nonzero" d="M15.723 8.354l-4.17-4.17a.633.633 0 0 0-.893.894l3.134 3.133H5.632a.632.632 0 0 0 0 1.264h8.162l-3.091 3.091a.632.632 0 0 0 .893.893l4.17-4.17a.605.605 0 0 0 .041-.045c.007-.007.011-.015.017-.023l.02-.027c.006-.01.01-.019.017-.028l.015-.025.013-.029c.005-.009.01-.018.013-.027l.01-.028.011-.03.007-.029.008-.031.005-.034.004-.027a.64.64 0 0 0-.004-.152l-.005-.033-.008-.032-.007-.029-.01-.03a.249.249 0 0 0-.01-.028l-.014-.027-.013-.029-.015-.025-.017-.028c-.006-.01-.013-.018-.02-.027l-.017-.023a.61.61 0 0 0-.065-.067.304.304 0 0 0-.019-.022zM1 0h8.614v1.374h-8.24V16.48h8.24v1.373H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1z"/>
+</svg>
diff --git a/modules/frontend/public/images/icons/expand.icon.svg b/modules/frontend/public/images/icons/expand.icon.svg
new file mode 100644
index 0000000..131378e
--- /dev/null
+++ b/modules/frontend/public/images/icons/expand.icon.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg">
+ <path d="m2 0c-1.108 0-2 0.892-2 2v9c0 1.108 0.892 2 2 2h9c1.108 0 2-0.892 2-2v-9c0-1.108-0.892-2-2-2zm4 3h1v3h3v1h-3v3h-1v-3h-3v-1h3z" fill="currentColor" />
+</svg>
diff --git a/modules/frontend/public/images/icons/eyeClosed.icon.svg b/modules/frontend/public/images/icons/eyeClosed.icon.svg
new file mode 100644
index 0000000..32bcba8
--- /dev/null
+++ b/modules/frontend/public/images/icons/eyeClosed.icon.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="8" viewBox="0 0 18 8">
+    <g fill="currentColor" fill-rule="nonzero">
+        <path d="M.562 1.241l.876-.482A8.577 8.577 0 0 0 8.96 5.193 8.55 8.55 0 0 0 16.481.76l.878.48A9.55 9.55 0 0 1 8.96 6.193 9.577 9.577 0 0 1 .562 1.241z"/>
+        <path d="M1.59 4.936c-.155.21-.427.32-.65.157-.222-.163-.198-.456-.043-.667l1.137-1.545c.155-.21.427-.32.65-.157.222.164.198.457.043.667L1.59 4.936zM3.792 6.545c-.136.223-.398.356-.633.212-.236-.144-.237-.439-.1-.661l1-1.637c.136-.223.399-.356.634-.212.236.144.237.439.1.66l-1 1.638zM5.862 7.834c-.254-.106-.3-.397-.2-.637l.741-1.77c.101-.242.34-.413.594-.306.255.107.3.397.2.637l-.74 1.77c-.102.242-.34.413-.595.306zM16.484 5.229l-1.137-1.546c-.155-.21-.179-.503.044-.667.223-.162.494-.053.65.158l1.136 1.545c.155.21.18.503-.043.667-.223.163-.495.053-.65-.157zM14.282 6.838l-1-1.637c-.138-.223-.137-.517.099-.661.236-.144.497-.011.634.212l1 1.636c.137.223.136.518-.1.661-.235.144-.497.012-.633-.211zM11.575 7.574l-.74-1.77c-.101-.24-.056-.53.2-.638.254-.106.492.064.594.306l.74 1.77c.1.24.055.53-.2.637-.254.107-.493-.063-.594-.305z"/>
+    </g>
+</svg>
diff --git a/modules/frontend/public/images/icons/eyeOpened.icon.svg b/modules/frontend/public/images/icons/eyeOpened.icon.svg
new file mode 100644
index 0000000..74c3f78
--- /dev/null
+++ b/modules/frontend/public/images/icons/eyeOpened.icon.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="12" viewBox="0 0 18 12">
+    <g fill="currentColor" fill-rule="evenodd" transform="translate(1 1)">
+        <path fill-rule="nonzero" d="M16.428 5.418a9.567 9.567 0 0 1-8.402 4.95A9.566 9.566 0 0 1-.375 5.418a1.008 1.008 0 0 1 0-.97C1.295 1.405 4.505-.533 8.03-.5c2.576.025 4.734 1.039 6.439 2.694 1.219 1.183 2.214 2.756 1.96 3.224zm-1.29-.838a9.278 9.278 0 0 0-1.366-1.669C12.241 1.425 10.32.523 8.021.501 4.869.47 1.997 2.204.5 4.93a8.557 8.557 0 0 0 7.525 4.437c3.1 0 5.905-1.642 7.419-4.241a5.366 5.366 0 0 0-.307-.546z"/>
+        <path fill-rule="nonzero" d="M8 9.517a4.5 4.5 0 1 1 0-9 4.5 4.5 0 0 1 0 9zm0-1a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/>
+        <circle cx="8" cy="5" r="2"/>
+    </g>
+</svg>
diff --git a/modules/frontend/public/images/icons/filter.icon.svg b/modules/frontend/public/images/icons/filter.icon.svg
new file mode 100644
index 0000000..b924daf
--- /dev/null
+++ b/modules/frontend/public/images/icons/filter.icon.svg
@@ -0,0 +1 @@
+<svg id="filter-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor" transform="translate(-635 -112)"><g transform="translate(475 33)"><path d="M175.9 79.4c.1.3.1.6-.2.8l-5.6 5.6v8.4c0 .3-.1.5-.4.7-.1 0-.2.1-.3.1-.2 0-.4-.1-.5-.2l-2.9-2.9c-.1-.1-.2-.3-.2-.5v-5.5l-5.6-5.6c-.2-.2-.3-.5-.2-.8.1-.3.4-.4.7-.4h14.5c.4-.1.6 0 .7.3z"/></g></g></svg>
\ No newline at end of file
diff --git a/modules/frontend/public/images/icons/gear.icon.svg b/modules/frontend/public/images/icons/gear.icon.svg
new file mode 100644
index 0000000..83527c2
--- /dev/null
+++ b/modules/frontend/public/images/icons/gear.icon.svg
@@ -0,0 +1 @@
+<svg id="gear-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path d="M14.1 8.8c0-.3.1-.5.1-.8 0-.3 0-.5-.1-.8l1.7-1.3c.2-.1.2-.3.1-.5l-1.6-2.8c-.1-.2-.3-.2-.5-.2l-2 .8c-.4-.3-.9-.6-1.4-.8L10.1.3C10 .1 9.8 0 9.6 0H6.4c-.3 0-.4.1-.5.3l-.3 2.2c-.5.2-1 .5-1.4.8l-2-.8c-.2-.1-.4-.1-.5.1L.1 5.4c-.1.2-.1.4.1.5l1.7 1.3c0 .3-.1.5-.1.8 0 .3 0 .5.1.8L.2 10.1c-.2.1-.2.3-.1.5l1.6 2.8c.1.2.3.2.5.2l2-.8c.4.3.9.6 1.4.8l.3 2.1c.1.2.2.3.5.3h3.3c.2 0 .4-.1.4-.3l.3-2.1c.5-.2 1-.5 1.4-.8l2 .8c.2.1.4 0 .5-.2l1.6-2.8c.1-.2.1-.4-.1-.5l-1.7-1.3zM8 11c-1.7 0-3-1.3-3-3s1.3-3 3-3 3 1.3 3 3-1.3 3-3 3z"/></g></svg>
\ No newline at end of file
diff --git a/modules/frontend/public/images/icons/home.icon.svg b/modules/frontend/public/images/icons/home.icon.svg
new file mode 100644
index 0000000..64a4329
--- /dev/null
+++ b/modules/frontend/public/images/icons/home.icon.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <polyline id="Home-icon" transform="matrix(1.3333 0 0 1.3333 -12 -9.4667)" points="13.8 18.2 13.8 14.6 16.2 14.6 16.2 18.2 19.2 18.2 19.2 13.4 21 13.4 15 8 9 13.4 10.8 13.4 10.8 18.2 13.8 18.2" fill="currentColor" fill-rule="evenodd"/>
+</svg>
diff --git a/modules/frontend/public/images/icons/index.js b/modules/frontend/public/images/icons/index.js
new file mode 100644
index 0000000..2e41085
--- /dev/null
+++ b/modules/frontend/public/images/icons/index.js
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {default as alert} from './alert.icon.svg';
+export {default as attention} from './attention.icon.svg';
+export {default as check} from './check.icon.svg';
+export {default as checkmark} from './checkmark.icon.svg';
+export {default as clock} from './clock.icon.svg';
+export {default as collapse} from './collapse.icon.svg';
+export {default as copy} from './copy.icon.svg';
+export {default as cross} from './cross.icon.svg';
+export {default as csv} from './csv.icon.svg';
+export {default as download} from './download.icon.svg';
+export {default as exclamation} from './exclamation.icon.svg';
+export {default as exit} from './exit.icon.svg';
+export {default as expand} from './expand.icon.svg';
+export {default as eyeClosed} from './eyeClosed.icon.svg';
+export {default as eyeOpened} from './eyeOpened.icon.svg';
+export {default as filter} from './filter.icon.svg';
+export {default as gear} from './gear.icon.svg';
+export {default as home} from './home.icon.svg';
+export {default as info} from './info.icon.svg';
+export {default as lockClosed} from './lockClosed.icon.svg';
+export {default as lockOpened} from './lockOpened.icon.svg';
+export {default as manual} from './manual.icon.svg';
+export {default as plus} from './plus.icon.svg';
+export {default as refresh} from './refresh.icon.svg';
+export {default as search} from './search.icon.svg';
+export {default as sort} from './sort.icon.svg';
+export {default as structure} from './structure.icon.svg';
+export {default as cluster} from './cluster.icon.svg';
+export {default as sql} from './sql.icon.svg';
+export {default as menu} from './menu.icon.svg';
+export {default as downloadAgent} from './downloadAgent.icon.svg';
diff --git a/modules/frontend/public/images/icons/info.icon.svg b/modules/frontend/public/images/icons/info.icon.svg
new file mode 100644
index 0000000..de92136
--- /dev/null
+++ b/modules/frontend/public/images/icons/info.icon.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <path fill="currentColor" d="m8 0c-4.4 0-8 3.6-8 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm0 0.80078c4 0 7.1992 3.1992 7.1992 7.1992s-3.1992 7.1992-7.1992 7.1992-7.1992-3.1992-7.1992-7.1992 3.1992-7.1992 7.1992-7.1992zm0 2.7988c-1.3 0-2.4004 1.1004-2.4004 2.4004 0 0.2 0.20039 0.40039 0.40039 0.40039s0.40039-0.20039 0.40039-0.40039c0-0.9 0.69961-1.5996 1.5996-1.5996s1.5996 0.69961 1.5996 1.5996-0.69961 1.5996-1.5996 1.5996c-0.2 0-0.40039 0.20039-0.40039 0.40039v1.6992c0 0.2 0.20039 0.40039 0.40039 0.40039s0.40039-0.098828 0.40039-0.29883v-1.4004c1.1-0.2 2-1.2004 2-2.4004 0-1.3-1.1004-2.4004-2.4004-2.4004zm0 7.5c-0.2 0-0.40039 0.20039-0.40039 0.40039v1.0996c0 0.2 0.20039 0.40039 0.40039 0.40039s0.40039-0.20039 0.40039-0.40039v-1.0996c0-0.2-0.20039-0.40039-0.40039-0.40039z"/>
+</svg>
diff --git a/modules/frontend/public/images/icons/lockClosed.icon.svg b/modules/frontend/public/images/icons/lockClosed.icon.svg
new file mode 100644
index 0000000..22f81e3
--- /dev/null
+++ b/modules/frontend/public/images/icons/lockClosed.icon.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="12" height="16" viewBox="0 0 12 16">
+    <path fill="currentColor" fill-rule="nonzero" d="M5.714 10.808v1.483a.286.286 0 0 0 .572 0v-1.483a.858.858 0 1 0-.572 0zm-4-4.522v-2a4.285 4.285 0 1 1 8.572 0v2A1.719 1.719 0 0 1 12 8.006v5.703c0 .956-.77 1.72-1.72 1.72H1.72c-.951 0-1.72-.77-1.72-1.72V8.005c0-.954.767-1.717 1.714-1.72zm1.715 0H8.57v-2A2.574 2.574 0 0 0 6 1.714a2.57 2.57 0 0 0-2.571 2.572v2z"/>
+</svg>
diff --git a/modules/frontend/public/images/icons/lockOpened.icon.svg b/modules/frontend/public/images/icons/lockOpened.icon.svg
new file mode 100644
index 0000000..bbc22c8
--- /dev/null
+++ b/modules/frontend/public/images/icons/lockOpened.icon.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="12" height="17" viewBox="0 0 12 17">
+    <path fill="currentColor" fill-rule="nonzero" d="M10.286 4.285v-.428.5c0 .587-.384 1.072-.857 1.072-.477 0-.858-.48-.858-1.072v-.5.428A2.573 2.573 0 0 0 6 1.715a2.57 2.57 0 0 0-2.571 2.57v3.001c0 .096.005.192.015.285H10.286A1.719 1.719 0 0 1 12 9.291v5.704c0 .955-.77 1.72-1.72 1.72H1.72c-.951 0-1.72-.77-1.72-1.72V9.29c0-.954.767-1.717 1.714-1.72V4.285a4.285 4.285 0 1 1 8.572 0zm-4.572 9.292a.286.286 0 0 0 .572 0v-1.483a.857.857 0 1 0-.572 0v1.483z"/>
+</svg>
diff --git a/modules/frontend/public/images/icons/manual.icon.svg b/modules/frontend/public/images/icons/manual.icon.svg
new file mode 100644
index 0000000..434bde4
--- /dev/null
+++ b/modules/frontend/public/images/icons/manual.icon.svg
@@ -0,0 +1 @@
+<svg id='manual-icon' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M10.7 8.3c.6 0 1.1-.5 1.1-1.1S11.3 6 10.7 6c-.7 0-1.2.5-1.2 1.1s.5 1.2 1.2 1.2zm-5.4 0c.6 0 1.1-.5 1.1-1.1S6 6 5.3 6s-1.1.5-1.1 1.1.5 1.2 1.1 1.2zM8 13.2c1.8 0 3.3-1.1 3.9-2.7H4.1c.6 1.6 2.1 2.7 3.9 2.7zm0 1.3c-3.1 0-5.6-2.5-5.6-5.6 0-3.1 2.5-5.6 5.6-5.6s5.6 2.5 5.6 5.6c0 3.1-2.5 5.6-5.6 5.6zM8 1.7C4 1.7.8 4.9.8 8.9S4 16.1 8 16.1s7.2-3.2 7.2-7.2S12 1.7 8 1.7z"/></svg>
\ No newline at end of file
diff --git a/modules/frontend/public/images/icons/menu.icon.svg b/modules/frontend/public/images/icons/menu.icon.svg
new file mode 100644
index 0000000..392d42e
--- /dev/null
+++ b/modules/frontend/public/images/icons/menu.icon.svg
@@ -0,0 +1,7 @@
+<svg width="16" height="16" version="1.1" viewBox="0 0 4.2333332 4.2333332" xmlns="http://www.w3.org/2000/svg">
+	<g transform="translate(0 -292.77)" fill="currentColor">
+		<rect y="295.95" width="4.2333" height=".52917" style="paint-order:markers fill stroke"/>
+		<rect x="1.1126e-7" y="294.62" width="4.2333" height=".52917" style="paint-order:markers fill stroke"/>
+		<rect y="293.3" width="4.2333" height=".52917" style="paint-order:markers fill stroke"/>
+	</g>
+</svg>
diff --git a/modules/frontend/public/images/icons/plus.icon.svg b/modules/frontend/public/images/icons/plus.icon.svg
new file mode 100644
index 0000000..04cdc47
--- /dev/null
+++ b/modules/frontend/public/images/icons/plus.icon.svg
@@ -0,0 +1,2 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
+<polyline fill="currentColor" points="12,6.9 6.9,6.9 6.9,12 5.1,12 5.1,6.9 0,6.9 0,5.1 5.1,5.1 5.1,0 6.9,0 6.9,5.1 12,5.1  12,6.9 "/></svg>
\ No newline at end of file
diff --git a/modules/frontend/public/images/icons/refresh.icon.svg b/modules/frontend/public/images/icons/refresh.icon.svg
new file mode 100644
index 0000000..4252e88
--- /dev/null
+++ b/modules/frontend/public/images/icons/refresh.icon.svg
@@ -0,0 +1 @@
+<svg id="refresh-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 20"><path d="M7 5.1v2.6l3.5-3.5L7 .7v2.6c-3.9 0-7 3.1-7 7C0 11.7.4 13 1.1 14l1.3-1.3c-.4-.7-.6-1.6-.6-2.5C1.7 7.4 4.1 5.1 7 5.1zm5.9 1.5l-1.3 1.3c.4.7.6 1.6.6 2.5 0 2.9-2.4 5.2-5.2 5.2V13l-3.5 3.5L7 20v-2.6c3.9 0 7-3.1 7-7 0-1.4-.4-2.7-1.1-3.8z" fill="currentColor"/></svg>
diff --git a/modules/frontend/public/images/icons/search.icon.svg b/modules/frontend/public/images/icons/search.icon.svg
new file mode 100644
index 0000000..32e594c
--- /dev/null
+++ b/modules/frontend/public/images/icons/search.icon.svg
@@ -0,0 +1 @@
+<svg id="search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><g stroke="currentColor" stroke-width="2" transform="translate(-156 -179)"><g transform="translate(0 33)"><g transform="translate(0 120)"><g transform="translate(157 27)"><circle fill="transparent" cx="5.7" cy="5.7" r="5.7"/><path d="M9.1 9.1L16 16"/></g></g></g></g></svg>
\ No newline at end of file
diff --git a/modules/frontend/public/images/icons/sort.icon.svg b/modules/frontend/public/images/icons/sort.icon.svg
new file mode 100644
index 0000000..8195c79
--- /dev/null
+++ b/modules/frontend/public/images/icons/sort.icon.svg
@@ -0,0 +1 @@
+<svg id="drag-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M 4,6.9 H 6.3 V 4.6 H 4 Z m 4.6,0 h 2.3 V 4.6 H 8.6 Z M 4,11.4 H 6.3 V 9.1 H 4 Z m 4.6,0 h 2.3 V 9.1 H 8.6 Z M 4,16 H 6.3 V 13.7 H 4 Z m 4.6,0 h 2.3 V 13.7 H 8.6 Z M 4,2.3 H 6.3 V 0 H 4 Z m 4.6,0 h 2.3 V 0 H 8.6 Z"/></svg>
diff --git a/modules/frontend/public/images/icons/sql.icon.svg b/modules/frontend/public/images/icons/sql.icon.svg
new file mode 100644
index 0000000..bd54eee
--- /dev/null
+++ b/modules/frontend/public/images/icons/sql.icon.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" viewBox="0 0 20 16" xmlns="http://www.w3.org/2000/svg">
+	<path d="M6.454 9.208c0-.2-.07-.353-.211-.46-.141-.107-.395-.22-.762-.338a5.4 5.4 0 0 1-.871-.351c-.583-.315-.875-.74-.875-1.273 0-.278.079-.525.235-.743a1.53 1.53 0 0 1 .673-.51c.293-.121.622-.183.986-.183.367 0 .694.067.98.2.287.133.51.32.669.562.158.242.238.517.238.825H6.458c0-.235-.074-.417-.222-.548-.148-.13-.357-.196-.625-.196-.258 0-.46.055-.603.164a.517.517 0 0 0-.215.432c0 .167.084.307.252.42.169.113.416.219.743.317.602.181 1.04.406 1.315.674.275.268.413.602.413 1.001 0 .445-.168.793-.505 1.046-.336.253-.788.38-1.357.38-.395 0-.755-.073-1.08-.218a1.708 1.708 0 0 1-.742-.594 1.522 1.522 0 0 1-.255-.875h1.061c0 .567.339.85 1.016.85.251 0 .448-.05.589-.153a.5.5 0 0 0 .211-.429zm5.988-1.103c0 .48-.077.896-.233 1.25-.155.354-.37.637-.645.851l.854.67-.674.596-1.093-.878a2.314 2.314 0 0 1-.388.032c-.426 0-.805-.102-1.14-.307a2.038 2.038 0 0 1-.775-.876c-.183-.38-.276-.816-.279-1.31v-.254c0-.506.091-.95.274-1.335.182-.384.44-.68.772-.885a2.124 2.124 0 0 1 1.14-.309c.429 0 .809.103 1.142.309.332.206.59.5.772.885.182.385.273.828.273 1.331v.23zm-1.072-.233c0-.538-.096-.948-.289-1.227-.193-.28-.468-.42-.825-.42-.355 0-.63.138-.822.414-.193.277-.29.682-.293 1.215v.25c0 .525.097.932.29 1.22.192.29.47.435.832.435.355 0 .628-.14.818-.418.19-.279.287-.685.29-1.219v-.25zm2.906 1.834h2.246v.85h-3.304V5.42h1.058v4.285zM18.182 0H1.818C.818 0 0 .8 0 1.778v12.444C0 15.2.818 16 1.818 16h16.364c1 0 1.818-.8 1.818-1.778V1.778C20 .8 19.182 0 18.182 0zM1.818 1.769h16.364V14.23H1.818V1.77z" fill="currentColor" fill-rule="evenodd"/>
+</svg>
diff --git a/modules/frontend/public/images/icons/structure.icon.svg b/modules/frontend/public/images/icons/structure.icon.svg
new file mode 100644
index 0000000..b83386e
--- /dev/null
+++ b/modules/frontend/public/images/icons/structure.icon.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" viewBox="0 0 16 14" xmlns="http://www.w3.org/2000/svg">
+ <path fill="currentColor" d="m0 0v3h1v3h4v2h2.0996v3.9004h3.9004v2.0996h5v-5h-5v1.9004h-2.9004v-2.9004h0.90039v-4h-4v1.0488l-3-0.048828v-2h1v-3h-3z"/>
+</svg>
diff --git a/modules/frontend/public/images/igfs.png b/modules/frontend/public/images/igfs.png
new file mode 100644
index 0000000..7de1f52
--- /dev/null
+++ b/modules/frontend/public/images/igfs.png
Binary files differ
diff --git a/modules/frontend/public/images/ignite-logo.svg b/modules/frontend/public/images/ignite-logo.svg
new file mode 100644
index 0000000..be2e72f
--- /dev/null
+++ b/modules/frontend/public/images/ignite-logo.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="90" height="39" version="1.1" viewBox="0 0 90 39" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <metadata>
+  <rdf:RDF>
+   <cc:Work rdf:about="">
+    <dc:format>image/svg+xml</dc:format>
+    <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+    <dc:title/>
+   </cc:Work>
+  </rdf:RDF>
+ </metadata>
+ <g fill-rule="evenodd">
+  <path d="m34.623 23.128c0 2.87 0.83 4.46 2.494 4.771 1.663 0.312 2.889-0.072 3.679-1.153 0.374-0.457 0.644-1.174 0.81-2.152a9.075 9.075 0 0 0 0.032 -2.9 5.796 5.796 0 0 0 -1.029 -2.556c-0.54-0.748-1.33-1.123-2.369-1.123-1.414 0-2.37 0.52-2.869 1.56-0.5 1.04-0.748 2.223-0.748 3.553m7.109 7.172v-1.06c-0.749 1.165-1.757 1.892-3.025 2.183-1.268 0.29-2.525 0.187-3.772-0.312-1.248-0.499-2.319-1.424-3.212-2.775-0.895-1.35-1.341-3.128-1.341-5.332 0-2.577 0.592-4.666 1.777-6.268 1.186-1.599 3.087-2.4 5.707-2.4 1.995 0 3.844 0.54 5.55 1.622 1.704 1.08 2.556 3.077 2.556 5.986v8.793c0 2.91-0.852 4.895-2.556 5.955a10.315 10.315 0 0 1 -5.55 1.59c-3.826 0-6.196-1.787-7.11-5.363h4.553c0.332 0.582 0.842 1.03 1.528 1.342 0.685 0.311 1.371 0.394 2.057 0.249 0.686-0.146 1.31-0.552 1.871-1.217 0.562-0.665 0.883-1.662 0.967-2.993m19.456 0.998h-2.183c-0.623 0-1.133-0.197-1.528-0.592s-0.592-0.905-0.592-1.528v-8.481c0-0.998-0.302-1.725-0.904-2.183a3.275 3.275 0 0 0 -2.027 -0.686c-0.748 0-1.414 0.229-1.996 0.686-0.582 0.458-0.872 1.185-0.872 2.183v10.601h-4.366v-9.728c0-2.827 0.737-4.718 2.213-5.675a9.675 9.675 0 0 1 4.896 -1.559c1.828 0 3.512 0.478 5.05 1.434 1.538 0.957 2.308 2.89 2.308 5.8v9.728zm1.06-15.147h4.366v15.147h-4.366zm12.402 15.147a32.414 32.414 0 0 1 -4.458 -0.063c-1.6-0.124-2.4-1.142-2.4-3.055v-16.228h2.244c0.54 0 1.018 0.208 1.435 0.623 0.414 0.416 0.623 0.915 0.623 1.497v0.586h2.557v0.872c0 0.625-0.208 1.134-0.624 1.529a2.094 2.094 0 0 1 -1.496 0.592h-0.437v9.344c0 0.708 0.415 1.06 1.247 1.06h1.31v3.243z"/>
+  <path d="m79.078 21.32h6.36c-0.082-1.122-0.373-1.954-0.872-2.494-0.54-0.582-1.309-0.873-2.307-0.873-0.915 0-1.663 0.291-2.245 0.873-0.583 0.582-0.894 1.414-0.936 2.494m7.484 4.865h3.118c-0.458 1.746-1.331 3.076-2.62 3.99-1.289 0.957-2.91 1.435-4.863 1.435-2.37 0-4.22-0.769-5.55-2.307-1.332-1.497-1.996-3.638-1.996-6.424 0-2.66 0.644-4.74 1.933-6.235 1.33-1.538 3.18-2.308 5.55-2.308 2.495 0 4.428 0.748 5.8 2.245 1.33 1.539 1.996 3.68 1.996 6.423v0.686c0 0.209-0.022 0.354-0.063 0.436h-10.789c0.042 1.29 0.353 2.245 0.936 2.869 0.54 0.666 1.35 0.998 2.432 0.998 0.707 0 1.31-0.145 1.808-0.436a0.875 0.875 0 0 0 0.344 -0.22l0.342-0.342c0.041-0.124 0.198-0.29 0.468-0.499 0.27-0.207 0.655-0.311 1.154-0.311m-61.482-17.658h2.245c0.581 0 1.08 0.208 1.496 0.623 0.415 0.417 0.624 0.915 0.624 1.497v20.651h-4.366v-22.771zm37.168 3.425h2.245c0.582 0 1.08 0.208 1.497 0.623 0.415 0.416 0.624 0.915 0.624 1.497v1.31h-4.366zm-26.818-1.739c0-0.384-0.13-0.712-0.387-0.983a1.263 1.263 0 0 0 -0.955 -0.407 1.36 1.36 0 0 0 -1.006 0.425c-0.263 0.266-0.394 0.55-0.394 0.852 0 0.398 0.122 0.737 0.366 1.019 0.254 0.295 0.578 0.443 0.972 0.443 0.377 0 0.706-0.131 0.985-0.393 0.279-0.263 0.419-0.58 0.419-0.956zm0.693 1.966h-0.693v-0.49c-0.35 0.384-0.821 0.576-1.414 0.576a2.01 2.01 0 0 1 -1.425 -0.556c-0.418-0.391-0.627-0.89-0.627-1.496 0-0.627 0.22-1.143 0.662-1.548 0.407-0.373 0.887-0.56 1.442-0.56 0.578 0 1.032 0.2 1.362 0.598v-0.501h0.693zm4.213-1.987c0-0.384-0.136-0.708-0.407-0.973a1.359 1.359 0 0 0 -0.983 -0.396c-0.4 0-0.73 0.145-0.988 0.436a1.386 1.386 0 0 0 -0.358 0.954c0 0.338 0.105 0.634 0.313 0.885 0.256 0.309 0.615 0.464 1.077 0.464 0.385 0 0.705-0.132 0.961-0.395 0.257-0.263 0.385-0.588 0.385-0.975m0.741-0.092c0 0.62-0.194 1.133-0.58 1.54-0.393 0.417-0.898 0.625-1.514 0.625-0.569 0-1.017-0.185-1.341-0.556v1.788h-0.725v-5.309h0.694v0.477c0.178-0.185 0.377-0.325 0.597-0.418 0.22-0.094 0.46-0.141 0.724-0.141a2.18 2.18 0 0 1 1.469 0.545c0.45 0.39 0.676 0.872 0.676 1.449m4.077 0.113c0-0.384-0.129-0.712-0.386-0.983a1.265 1.265 0 0 0 -0.956 -0.407c-0.39 0-0.727 0.142-1.005 0.425-0.264 0.266-0.395 0.55-0.395 0.852 0 0.398 0.122 0.737 0.367 1.019 0.254 0.295 0.578 0.443 0.971 0.443 0.378 0 0.706-0.131 0.985-0.393 0.28-0.263 0.42-0.58 0.42-0.956zm0.693 1.966h-0.693v-0.49c-0.35 0.384-0.822 0.576-1.414 0.576a2.012 2.012 0 0 1 -1.425 -0.556c-0.418-0.391-0.627-0.89-0.627-1.496 0-0.627 0.221-1.143 0.662-1.548 0.407-0.373 0.888-0.56 1.441-0.56 0.579 0 1.033 0.2 1.363 0.598v-0.501h0.693zm4.773-1.314c-0.237 0.462-0.504 0.804-0.804 1.026-0.336 0.25-0.746 0.374-1.229 0.374a2.066 2.066 0 0 1 -1.455 -0.583 2.01 2.01 0 0 1 -0.662 -1.53h0.031a0.4 0.4 0 0 1 -0.014 0.033c0-0.624 0.213-1.136 0.639-1.534a1.94 1.94 0 0 1 1.369 -0.545c0.49 0 0.918 0.125 1.285 0.376 0.367 0.25 0.643 0.604 0.826 1.062h-0.796c-0.272-0.48-0.705-0.72-1.3-0.72-0.374 0-0.687 0.132-0.936 0.397a1.353 1.353 0 0 0 -0.376 0.964c0 0.39 0.13 0.716 0.392 0.98 0.262 0.265 0.586 0.397 0.972 0.397 0.254 0 0.495-0.063 0.724-0.187 0.228-0.125 0.408-0.295 0.538-0.51h0.795zm4.13 1.318h-0.716v-2.286c0-0.31-0.1-0.565-0.295-0.763-0.197-0.198-0.452-0.297-0.763-0.297-0.304 0-0.55 0.091-0.74 0.274s-0.286 0.426-0.286 0.728v2.344h-0.724v-5.31h0.724v1.662c0.277-0.277 0.625-0.416 1.044-0.416 0.46 0 0.86 0.157 1.2 0.47 0.371 0.34 0.557 0.775 0.557 1.308v2.286zm3.97-2.375a1.372 1.372 0 0 0 -0.488 -0.722 1.297 1.297 0 0 0 -0.816 -0.28c-0.38 0-0.695 0.124-0.944 0.374a1.438 1.438 0 0 0 -0.367 0.628zm0.73 0.666h-3.359c0.057 0.343 0.224 0.616 0.501 0.82 0.245 0.178 0.51 0.268 0.793 0.268 0.27 0 0.522-0.076 0.758-0.227 0.245-0.156 0.414-0.356 0.508-0.6h0.738c-0.192 0.524-0.492 0.92-0.9 1.187-0.347 0.229-0.727 0.343-1.139 0.343-0.535 0-0.994-0.192-1.376-0.576-0.405-0.41-0.607-0.916-0.607-1.517 0-0.625 0.216-1.134 0.648-1.528a1.988 1.988 0 0 1 1.383 -0.538c0.538 0 1 0.177 1.387 0.532 0.444 0.405 0.665 0.948 0.665 1.63z"/>
+  <path d="m14.941 0.14844s-16.913 6.5058-0.21875 18.049c5.98 4.135 4.9794 8.3509 4.9824 8.5059 0.945-0.975 5.7088-6.274 0.048828-12.209-5.66-5.934-7.2325-9.5967-4.8125-14.346zm-8.8359 6.4648s-13.215 8.0563-0.76172 16.07c2.403 1.553 11.088 5.9054 10.832 9.8984 0 0 5.2612-7.8044-2.4258-12.693-7.91-5.029-9.8905-10.378-7.6445-13.275zm-4.5996 15.406s-4.7235 7.1573 4.5605 8.6133c0.907 0.117 7.6331 0.81362 9.3691 3.0156 0 0-0.16105-3.801-5.623-6.043s-7.6996-2.7989-8.3066-5.5859z" fill="#f90314"/>
+ </g>
+</svg>
diff --git a/modules/frontend/public/images/ignite-puzzle.png b/modules/frontend/public/images/ignite-puzzle.png
new file mode 100644
index 0000000..0989d29
--- /dev/null
+++ b/modules/frontend/public/images/ignite-puzzle.png
Binary files differ
diff --git a/modules/frontend/public/images/multicluster.png b/modules/frontend/public/images/multicluster.png
new file mode 100644
index 0000000..ef86c3f
--- /dev/null
+++ b/modules/frontend/public/images/multicluster.png
Binary files differ
diff --git a/modules/frontend/public/images/page-landing-ui-sample.png b/modules/frontend/public/images/page-landing-ui-sample.png
new file mode 100644
index 0000000..d749875
--- /dev/null
+++ b/modules/frontend/public/images/page-landing-ui-sample.png
Binary files differ
diff --git a/modules/frontend/public/images/pb-ignite.png b/modules/frontend/public/images/pb-ignite.png
new file mode 100644
index 0000000..55f6746
--- /dev/null
+++ b/modules/frontend/public/images/pb-ignite.png
Binary files differ
diff --git a/modules/frontend/public/images/preview.png b/modules/frontend/public/images/preview.png
new file mode 100644
index 0000000..76275ec
--- /dev/null
+++ b/modules/frontend/public/images/preview.png
Binary files differ
diff --git a/modules/frontend/public/images/query-table.png b/modules/frontend/public/images/query-table.png
new file mode 100644
index 0000000..be4da8f
--- /dev/null
+++ b/modules/frontend/public/images/query-table.png
Binary files differ
diff --git a/modules/frontend/public/stylesheets/_bootstrap-custom.scss b/modules/frontend/public/stylesheets/_bootstrap-custom.scss
new file mode 100644
index 0000000..3dc338f
--- /dev/null
+++ b/modules/frontend/public/stylesheets/_bootstrap-custom.scss
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Core variables and mixins
+@import "bootstrap-variables";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/mixins";
+
+// Reset and dependencies
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/normalize";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/print";
+
+// Core CSS
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/scaffolding";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/type";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/code";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/grid";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/tables";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/forms";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/buttons";
+
+// Components
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/component-animations";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/dropdowns";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/button-groups";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/input-groups";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/navs";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/navbar";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/pagination";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/pager";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/labels";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/badges";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/jumbotron";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/thumbnails";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/alerts";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/progress-bars";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/media";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/list-group";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/panels";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/responsive-embed";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/wells";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/close";
+
+// Components w/ JavaScript
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/modals";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/tooltip";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/popovers";
+
+// Utility classes
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/utilities";
+@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/responsive-utilities";
diff --git a/modules/frontend/public/stylesheets/_bootstrap-variables.scss b/modules/frontend/public/stylesheets/_bootstrap-variables.scss
new file mode 100644
index 0000000..ea3dbf0
--- /dev/null
+++ b/modules/frontend/public/stylesheets/_bootstrap-variables.scss
@@ -0,0 +1,891 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+$bootstrap-sass-asset-helper: false !default;
+//
+// Variables
+// --------------------------------------------------
+
+
+//== Colors
+//
+//## Gray and brand colors for use across Bootstrap.
+
+$gray-base:              #000 !default;
+$gray-darker:            lighten($gray-base, 13.5%) !default; // #222
+$gray-dark:              lighten($gray-base, 20%) !default;   // #333
+$gray:                   lighten($gray-base, 33.5%) !default; // #555
+$gray-light:             lighten($gray-base, 46%) !default;   // #757575
+$gray-lighter:           lighten($gray-base, 93.5%) !default; // #eee
+
+$brand-primary:         #ee2b27 !default;
+$brand-success:         #50af51 !default;
+$brand-info:            #0067b9 !default;
+$brand-warning:         #f0ad4e !default;
+$brand-danger:          #d9534f !default;
+
+
+//== Scaffolding
+//
+//## Settings for some of the most global styles.
+
+//** Background color for `<body>`.
+$body-bg:               #f9f9f9 !default;
+//** Global text color on `<body>`.
+$text-color:            #393939 !default;
+
+//** Global textual link color.
+$link-color:            $brand-info !default;
+//** Link hover color set via `darken()` function.
+$link-hover-color:      darken($link-color, 15%) !default;
+//** Link hover decoration.
+$link-hover-decoration: underline !default;
+
+
+//== Typography
+//
+//## Font, line-height, and color for body text, headings, and more.
+
+$font-family-sans-serif:  var(--sans-serif-font), sans-serif !default;
+$font-family-serif:       var(--serif-font), serif !default;
+//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
+$font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace !default;
+$font-family-base:        $font-family-sans-serif !default;
+
+$font-size-base:          14px !default;
+$font-size-large:         ceil(($font-size-base * 1.25)) !default; // ~18px
+$font-size-small:         ceil(($font-size-base * 0.85)) !default; // ~12px
+
+$font-size-h1:            floor(($font-size-base * 2.6)) !default; // ~36px
+$font-size-h2:            floor(($font-size-base * 2.15)) !default; // ~30px
+$font-size-h3:            ceil(($font-size-base * 1.7)) !default; // ~24px
+$font-size-h4:            ceil(($font-size-base * 1.25)) !default; // ~18px
+$font-size-h5:            $font-size-base !default;
+$font-size-h6:            ceil(($font-size-base * 0.85)) !default; // ~12px
+
+//** Unit-less `line-height` for use in components like buttons.
+$line-height-base:        1.428571429 !default; // 20/14
+//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
+$line-height-computed:    floor(($font-size-base * $line-height-base)) !default; // ~20px
+
+//** By default, this inherits from the `<body>`.
+$headings-font-family:    inherit !default;
+$headings-font-weight:    500 !default;
+$headings-line-height:    1.1 !default;
+$headings-color:          inherit !default;
+
+
+//== Iconography
+//
+//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
+
+//** Load fonts from this directory.
+
+// [converter] If $bootstrap-sass-asset-helper if used, provide path relative to the assets load path.
+// [converter] This is because some asset helpers, such as Sprockets, do not work with file-relative paths.
+$icon-font-path: if($bootstrap-sass-asset-helper, "bootstrap/", "../fonts/bootstrap/") !default;
+
+//** File name for all font files.
+$icon-font-name:          "glyphicons-halflings-regular" !default;
+//** Element ID within SVG icon file.
+$icon-font-svg-id:        "glyphicons_halflingsregular" !default;
+
+
+//== Components
+//
+//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
+
+$padding-base-vertical:     6px !default;
+$padding-base-horizontal:   12px !default;
+
+$padding-large-vertical:    10px !default;
+$padding-large-horizontal:  16px !default;
+
+$padding-small-vertical:    5px !default;
+$padding-small-horizontal:  10px !default;
+
+$padding-xs-vertical:       1px !default;
+$padding-xs-horizontal:     5px !default;
+
+$line-height-large:         1.3333333 !default; // extra decimals for Win 8.1 Chrome
+$line-height-small:         1.5 !default;
+
+$border-radius-base:        4px !default;
+$border-radius-large:       6px !default;
+$border-radius-small:       3px !default;
+
+//** Global color for active items (e.g., navs or dropdowns).
+$component-active-color:    $link-color !default;
+//** Global background color for active items (e.g., navs or dropdowns).
+$component-active-bg:       $brand-primary !default;
+
+//** Width of the `border` for generating carets that indicator dropdowns.
+$caret-width-base:          4px !default;
+//** Carets increase slightly in size for larger components.
+$caret-width-large:         5px !default;
+
+
+//== Tables
+//
+//## Customizes the `.table` component with basic values, each used across all table variations.
+
+//** Padding for `<th>`s and `<td>`s.
+$table-cell-padding:            8px !default;
+//** Padding for cells in `.table-condensed`.
+$table-condensed-cell-padding:  5px !default;
+
+//** Default background color used for all tables.
+$table-bg:                      transparent !default;
+//** Background color used for `.table-striped`.
+$table-bg-accent:               #f9f9f9 !default;
+//** Background color used for `.table-hover`.
+$table-bg-hover:                #f5f5f5 !default;
+$table-bg-active:               $table-bg-hover !default;
+
+//** Border color for table and cell borders.
+$table-border-color:            #ddd !default;
+
+
+//== Buttons
+//
+//## For each of Bootstrap's buttons, define text, background and border color.
+
+$btn-font-weight:                normal !default;
+
+$btn-default-color:              #333 !default;
+$btn-default-bg:                 #fff !default;
+$btn-default-border:             #ccc !default;
+
+$btn-primary-color:              #fff !default;
+$btn-primary-bg:                 $brand-primary !default;
+$btn-primary-border:             darken($btn-primary-bg, 15%) !default;
+
+$btn-success-color:              #fff !default;
+$btn-success-bg:                 $brand-success !default;
+$btn-success-border:             darken($btn-success-bg, 5%) !default;
+
+$btn-info-color:                 #fff !default;
+$btn-info-bg:                    $brand-info !default;
+$btn-info-border:                darken($btn-info-bg, 5%) !default;
+
+$btn-warning-color:              #fff !default;
+$btn-warning-bg:                 $brand-warning !default;
+$btn-warning-border:             darken($btn-warning-bg, 5%) !default;
+
+$btn-danger-color:               #fff !default;
+$btn-danger-bg:                  $brand-danger !default;
+$btn-danger-border:              darken($btn-danger-bg, 5%) !default;
+
+$btn-link-disabled-color:        $gray-light !default;
+
+// Allows for customizing button radius independently from global border radius
+$btn-border-radius-base:         $border-radius-base !default;
+$btn-border-radius-large:        $border-radius-large !default;
+$btn-border-radius-small:        $border-radius-small !default;
+
+
+//== Forms
+//
+//##
+
+//** `<input>` background color
+$input-bg:                       #fff !default;
+//** `<input disabled>` background color
+$input-bg-disabled:              $gray-lighter !default;
+
+//** Text color for `<input>`s
+$input-color:                    $gray !default;
+//** `<input>` border color
+$input-border:                   #ccc !default;
+
+// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
+//** Default `.form-control` border radius
+// This has no effect on `<select>`s in some browsers, due to the limited stylability of `<select>`s in CSS.
+$input-border-radius:            $border-radius-base !default;
+//** Large `.form-control` border radius
+$input-border-radius-large:      $border-radius-large !default;
+//** Small `.form-control` border radius
+$input-border-radius-small:      $border-radius-small !default;
+
+//** Border color for inputs on focus
+$input-border-focus:             #66afe9 !default;
+
+//** Placeholder text color
+$input-color-placeholder:        #999 !default;
+
+//** Default `.form-control` height
+$input-height-base:              ($line-height-computed + ($padding-base-vertical * 2) + 2) !default;
+//** Large `.form-control` height
+$input-height-large:             (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2) !default;
+//** Small `.form-control` height
+$input-height-small:             (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2) !default;
+
+//** `.form-group` margin
+$form-group-margin-bottom:       15px !default;
+
+$legend-color:                   $gray-dark !default;
+$legend-border-color:            #e5e5e5 !default;
+
+//** Background color for textual input addons
+$input-group-addon-bg:           $gray-lighter !default;
+//** Border color for textual input addons
+$input-group-addon-border-color: $input-border !default;
+
+//** Disabled cursor for form controls and buttons.
+$cursor-disabled:                not-allowed !default;
+
+
+//== Dropdowns
+//
+//## Dropdown menu container and contents.
+
+//** Background for the dropdown menu.
+$dropdown-bg:                    #fff !default;
+//** Dropdown menu `border-color`.
+$dropdown-border:                rgba(0,0,0,.15) !default;
+//** Dropdown menu `border-color` **for IE8**.
+$dropdown-fallback-border:       #ccc !default;
+//** Divider color for between dropdown items.
+$dropdown-divider-bg:            #e5e5e5 !default;
+
+//** Dropdown link text color.
+$dropdown-link-color:            #555 !default;
+//** Hover color for dropdown links.
+$dropdown-link-hover-color:      $link-hover-color !default;
+//** Hover background for dropdown links.
+$dropdown-link-hover-bg:         transparent !default;
+
+//** Active dropdown menu item text color.
+$dropdown-link-active-color:     $component-active-color !default;
+//** Active dropdown menu item background color.
+$dropdown-link-active-bg:        transparent !default;
+
+//** Disabled dropdown menu item background color.
+$dropdown-link-disabled-color:   $gray-light !default;
+
+//** Text color for headers within dropdown menus.
+$dropdown-header-color:          $gray-light !default;
+
+//** Deprecated `$dropdown-caret-color` as of v3.1.0
+$dropdown-caret-color:           #000 !default;
+
+
+//-- Z-index master list
+//
+// Warning: Avoid customizing these values. They're used for a bird's eye view
+// of components dependent on the z-axis and are designed to all work together.
+//
+// Note: These variables are not generated into the Customizer.
+
+$zindex-navbar:            1000 !default;
+$zindex-dropdown:          1002 !default;
+$zindex-popover:           1060 !default;
+$zindex-tooltip:           1070 !default;
+$zindex-navbar-fixed:      1030 !default;
+$zindex-modal-background:  1040 !default;
+$zindex-modal:             1050 !default;
+
+
+//== Media queries breakpoints
+//
+//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
+
+// Extra small screen / phone
+//** Deprecated `$screen-xs` as of v3.0.1
+$screen-xs:                  480px !default;
+//** Deprecated `$screen-xs-min` as of v3.2.0
+$screen-xs-min:              $screen-xs !default;
+//** Deprecated `$screen-phone` as of v3.0.1
+$screen-phone:               $screen-xs-min !default;
+
+// Small screen / tablet
+//** Deprecated `$screen-sm` as of v3.0.1
+$screen-sm:                  768px !default;
+$screen-sm-min:              $screen-sm !default;
+//** Deprecated `$screen-tablet` as of v3.0.1
+$screen-tablet:              $screen-sm-min !default;
+
+// Medium screen / desktop
+//** Deprecated `$screen-md` as of v3.0.1
+$screen-md:                  992px !default;
+$screen-md-min:              $screen-md !default;
+//** Deprecated `$screen-desktop` as of v3.0.1
+$screen-desktop:             $screen-md-min !default;
+
+// Large screen / wide desktop
+//** Deprecated `$screen-lg` as of v3.0.1
+$screen-lg:                  1200px !default;
+$screen-lg-min:              $screen-lg !default;
+//** Deprecated `$screen-lg-desktop` as of v3.0.1
+$screen-lg-desktop:          $screen-lg-min !default;
+
+// So media queries don't overlap when required, provide a maximum
+$screen-xs-max:              ($screen-sm-min - 1) !default;
+$screen-sm-max:              ($screen-md-min - 1) !default;
+$screen-md-max:              ($screen-lg-min - 1) !default;
+
+
+//== Grid system
+//
+//## Define your custom responsive grid.
+
+//** Number of columns in the grid.
+$grid-columns:              12 !default;
+//** Padding between columns. Gets divided in half for the left and right.
+$grid-gutter-width:         0 !default;
+// Navbar collapse
+//** Point at which the navbar becomes uncollapsed.
+$grid-float-breakpoint:     $screen-sm-min !default;
+//** Point at which the navbar begins collapsing.
+$grid-float-breakpoint-max: ($grid-float-breakpoint - 1) !default;
+
+
+//== Container sizes
+//
+//## Define the maximum width of `.container` for different screen sizes.
+
+// Small screen / tablet
+$container-tablet:             (720px + $grid-gutter-width) !default;
+//** For `$screen-sm-min` and up.
+$container-sm:                 $container-tablet !default;
+
+// Medium screen / desktop
+$container-desktop:            (940px + $grid-gutter-width) !default;
+//** For `$screen-md-min` and up.
+$container-md:                 $container-desktop !default;
+
+// Large screen / wide desktop
+$container-large-desktop:      (1140px + $grid-gutter-width) !default;
+//** For `$screen-lg-min` and up.
+$container-lg:                 $container-large-desktop !default;
+
+
+//== Navbar
+//
+//##
+
+// Basics of a navbar
+$navbar-height:                    50px !default;
+$navbar-margin-bottom:             $line-height-computed !default;
+$navbar-border-radius:             $border-radius-base !default;
+$navbar-padding-horizontal:        floor(($grid-gutter-width / 2)) !default;
+$navbar-padding-vertical:          (($navbar-height - $line-height-computed) / 2) !default;
+$navbar-collapse-max-height:       340px !default;
+
+$navbar-default-color:             #bbb !default;
+$navbar-default-bg:                #f8f8f8 !default;
+$navbar-default-border:            darken($navbar-default-bg, 6.5%) !default;
+
+// Navbar links
+$navbar-default-link-color:                #bbb !default;
+$navbar-default-link-hover-color:          $link-hover-color !default;
+$navbar-default-link-hover-bg:             transparent !default;
+$navbar-default-link-active-color:         $component-active-color !default;
+$navbar-default-link-active-bg:            darken($navbar-default-bg, 6.5%) !default;
+$navbar-default-link-disabled-color:       #ccc !default;
+$navbar-default-link-disabled-bg:          transparent !default;
+
+// Navbar brand label
+$navbar-default-brand-color:               $navbar-default-link-color !default;
+$navbar-default-brand-hover-color:         darken($link-color, 15%) !default;
+$navbar-default-brand-hover-bg:            transparent !default;
+
+// Navbar toggle
+$navbar-default-toggle-hover-bg:           #ddd !default;
+$navbar-default-toggle-icon-bar-bg:        #888 !default;
+$navbar-default-toggle-border-color:       #ddd !default;
+
+
+//=== Inverted navbar
+// Reset inverted navbar basics
+$navbar-inverse-color:                      lighten($gray-light, 15%) !default;
+$navbar-inverse-bg:                         #222 !default;
+$navbar-inverse-border:                     darken($navbar-inverse-bg, 10%) !default;
+
+// Inverted navbar links
+$navbar-inverse-link-color:                 lighten($gray-light, 15%) !default;
+$navbar-inverse-link-hover-color:           #fff !default;
+$navbar-inverse-link-hover-bg:              transparent !default;
+$navbar-inverse-link-active-color:          $navbar-inverse-link-hover-color !default;
+$navbar-inverse-link-active-bg:             darken($navbar-inverse-bg, 10%) !default;
+$navbar-inverse-link-disabled-color:        #444 !default;
+$navbar-inverse-link-disabled-bg:           transparent !default;
+
+// Inverted navbar brand label
+$navbar-inverse-brand-color:                $navbar-inverse-link-color !default;
+$navbar-inverse-brand-hover-color:          #fff !default;
+$navbar-inverse-brand-hover-bg:             transparent !default;
+
+// Inverted navbar toggle
+$navbar-inverse-toggle-hover-bg:            #333 !default;
+$navbar-inverse-toggle-icon-bar-bg:         #fff !default;
+$navbar-inverse-toggle-border-color:        #333 !default;
+
+
+//== Navs
+//
+//##
+
+//=== Shared nav styles
+$nav-link-padding:                          0 0 0 15px !default;
+$nav-link-hover-bg:                         transparent !default;
+
+$nav-disabled-link-color:                   $gray-light !default;
+$nav-disabled-link-hover-color:             $gray-light !default;
+
+//== Tabs
+$nav-tabs-border-color:                     #ddd !default;
+
+$nav-tabs-link-hover-border-color:          $gray-lighter !default;
+
+$nav-tabs-active-link-hover-bg:             $body-bg !default;
+$nav-tabs-active-link-hover-color:          $gray !default;
+$nav-tabs-active-link-hover-border-color:   #ddd !default;
+
+$nav-tabs-justified-link-border-color:            #ddd !default;
+$nav-tabs-justified-active-link-border-color:     $body-bg !default;
+
+//== Pills
+$nav-pills-border-radius:                   $border-radius-base !default;
+$nav-pills-active-link-hover-bg:            $component-active-bg !default;
+$nav-pills-active-link-hover-color:         $component-active-color !default;
+
+
+//== Pagination
+//
+//##
+
+$pagination-color:                     $link-color !default;
+$pagination-bg:                        #fff !default;
+$pagination-border:                    #ddd !default;
+
+$pagination-hover-color:               $link-hover-color !default;
+$pagination-hover-bg:                  $gray-lighter !default;
+$pagination-hover-border:              #ddd !default;
+
+$pagination-active-color:              #fff !default;
+$pagination-active-bg:                 $brand-primary !default;
+$pagination-active-border:             $brand-primary !default;
+
+$pagination-disabled-color:            $gray-light !default;
+$pagination-disabled-bg:               #fff !default;
+$pagination-disabled-border:           #ddd !default;
+
+
+//== Pager
+//
+//##
+
+$pager-bg:                             $pagination-bg !default;
+$pager-border:                         $pagination-border !default;
+$pager-border-radius:                  15px !default;
+
+$pager-hover-bg:                       $pagination-hover-bg !default;
+
+$pager-active-bg:                      $pagination-active-bg !default;
+$pager-active-color:                   $pagination-active-color !default;
+
+$pager-disabled-color:                 $pagination-disabled-color !default;
+
+
+//== Jumbotron
+//
+//##
+
+$jumbotron-padding:              30px !default;
+$jumbotron-color:                inherit !default;
+$jumbotron-bg:                   $gray-lighter !default;
+$jumbotron-heading-color:        inherit !default;
+$jumbotron-font-size:            ceil(($font-size-base * 1.5)) !default;
+$jumbotron-heading-font-size:    ceil(($font-size-base * 4.5)) !default;
+
+
+//== Form states and alerts
+//
+//## Define colors for form feedback states and, by default, alerts.
+
+$state-success-text:             #3c763d !default;
+$state-success-bg:               #dff0d8 !default;
+$state-success-border:           darken(adjust-hue($state-success-bg, -10), 25%) !default;
+
+$state-info-text:                #31708f !default;
+$state-info-bg:                  #d9edf7 !default;
+$state-info-border:              darken(adjust-hue($state-info-bg, -10), 25%) !default;
+
+$state-warning-text:             #8a6d3b !default;
+$state-warning-bg:               #fcf8e3 !default;
+$state-warning-border:           darken(adjust-hue($state-warning-bg, -10), 25%) !default;
+
+$state-danger-text:              #a94442 !default;
+$state-danger-bg:                #f2dede !default;
+$state-danger-border:            darken(adjust-hue($state-danger-bg, -10), 25%) !default;
+
+
+//== Tooltips
+//
+//##
+
+//** Tooltip max width
+$tooltip-max-width:           400px !default;
+//** Tooltip text color
+$tooltip-color:               #000 !default;
+//** Tooltip background color
+$tooltip-bg:                  #f5f5f5 !default;
+$tooltip-opacity:             1 !default;
+
+//** Tooltip arrow width
+$tooltip-arrow-width:         5px !default;
+//** Tooltip arrow color
+$tooltip-arrow-color:         #ccc !default;
+
+
+//== Popovers
+//
+//##
+
+//** Popover body background color
+$popover-bg:                          #fff !default;
+//** Popover maximum width
+$popover-max-width:                   auto !default;
+//** Popover border color
+$popover-border-color:                rgba(0,0,0,.2) !default;
+//** Popover fallback border color
+$popover-fallback-border-color:       #ccc !default;
+
+//** Popover title background color
+$popover-title-bg:                    darken($popover-bg, 3%) !default;
+
+//** Popover arrow width
+$popover-arrow-width:                 10px !default;
+//** Popover arrow color
+$popover-arrow-color:                 $popover-bg !default;
+
+//** Popover outer arrow width
+$popover-arrow-outer-width:           ($popover-arrow-width + 1) !default;
+//** Popover outer arrow color
+$popover-arrow-outer-color:           fade_in($popover-border-color, 0.05) !default;
+//** Popover outer arrow fallback color
+$popover-arrow-outer-fallback-color:  darken($popover-fallback-border-color, 20%) !default;
+
+
+//== Labels
+//
+//##
+
+//** Default label background color
+$label-default-bg:            $gray-light !default;
+//** Primary label background color
+$label-primary-bg:            $brand-primary !default;
+//** Success label background color
+$label-success-bg:            $brand-success !default;
+//** Info label background color
+$label-info-bg:               $brand-info !default;
+//** Warning label background color
+$label-warning-bg:            $brand-warning !default;
+//** Danger label background color
+$label-danger-bg:             $brand-danger !default;
+
+//** Default label text color
+$label-color:                 #fff !default;
+//** Default text color of a linked label
+$label-link-hover-color:      #fff !default;
+
+
+//== Modals
+//
+//##
+
+//** Padding applied to the modal body
+$modal-inner-padding:         15px !default;
+
+//** Padding applied to the modal title
+$modal-title-padding:         15px !default;
+//** Modal title line-height
+$modal-title-line-height:     $line-height-base !default;
+
+//** Background color of modal content area
+$modal-content-bg:                             #fff !default;
+//** Modal content border color
+$modal-content-border-color:                   rgba(0,0,0,.2) !default;
+//** Modal content border color **for IE8**
+$modal-content-fallback-border-color:          #999 !default;
+
+//** Modal backdrop background color
+$modal-backdrop-bg:           #000 !default;
+//** Modal backdrop opacity
+$modal-backdrop-opacity:      .5 !default;
+//** Modal header border color
+$modal-header-border-color:   #e5e5e5 !default;
+//** Modal footer border color
+$modal-footer-border-color:   $modal-header-border-color !default;
+
+$modal-lg:                    900px !default;
+$modal-md:                    600px !default;
+$modal-sm:                    300px !default;
+
+
+//== Alerts
+//
+//## Define alert colors, border radius, and padding.
+
+$alert-padding:               15px !default;
+$alert-border-radius:         $border-radius-base !default;
+$alert-link-font-weight:      bold !default;
+
+$alert-success-bg:            $state-success-bg !default;
+$alert-success-text:          $state-success-text !default;
+$alert-success-border:        $state-success-border !default;
+
+$alert-info-bg:               $state-info-bg !default;
+$alert-info-text:             $state-info-text !default;
+$alert-info-border:           $state-info-border !default;
+
+$alert-warning-bg:            $state-warning-bg !default;
+$alert-warning-text:          $state-warning-text !default;
+$alert-warning-border:        $state-warning-border !default;
+
+$alert-danger-bg:             $state-danger-bg !default;
+$alert-danger-text:           $state-danger-text !default;
+$alert-danger-border:         $state-danger-border !default;
+
+
+//== Progress bars
+//
+//##
+
+//** Background color of the whole progress component
+$progress-bg:                 #f5f5f5 !default;
+//** Progress bar text color
+$progress-bar-color:          #fff !default;
+//** Variable for setting rounded corners on progress bar.
+$progress-border-radius:      $border-radius-base !default;
+
+//** Default progress bar color
+$progress-bar-bg:             $brand-primary !default;
+//** Success progress bar color
+$progress-bar-success-bg:     $brand-success !default;
+//** Warning progress bar color
+$progress-bar-warning-bg:     $brand-warning !default;
+//** Danger progress bar color
+$progress-bar-danger-bg:      $brand-danger !default;
+//** Info progress bar color
+$progress-bar-info-bg:        $brand-info !default;
+
+
+//== List group
+//
+//##
+
+//** Background color on `.list-group-item`
+$list-group-bg:                 #fff !default;
+//** `.list-group-item` border color
+$list-group-border:             #ddd !default;
+//** List group border radius
+$list-group-border-radius:      $border-radius-base !default;
+
+//** Background color of single list items on hover
+$list-group-hover-bg:           #f5f5f5 !default;
+//** Text color of active list items
+$list-group-active-color:       $component-active-color !default;
+//** Background color of active list items
+$list-group-active-bg:          $component-active-bg !default;
+//** Border color of active list elements
+$list-group-active-border:      $list-group-active-bg !default;
+//** Text color for content within active list items
+$list-group-active-text-color:  lighten($list-group-active-bg, 40%) !default;
+
+//** Text color of disabled list items
+$list-group-disabled-color:      $gray-light !default;
+//** Background color of disabled list items
+$list-group-disabled-bg:         $gray-lighter !default;
+//** Text color for content within disabled list items
+$list-group-disabled-text-color: $list-group-disabled-color !default;
+
+$list-group-link-color:         #555 !default;
+$list-group-link-hover-color:   $list-group-link-color !default;
+$list-group-link-heading-color: #333 !default;
+
+
+//== Panels
+//
+//##
+
+$panel-bg:                    #fff !default;
+$panel-body-padding:          15px !default;
+$panel-heading-padding:       5px 10px !default;
+$panel-footer-padding:        $panel-heading-padding !default;
+$panel-border-radius:         $border-radius-base !default;
+
+//** Border color for elements within panels
+$panel-inner-border:          #ddd !default;
+$panel-footer-bg:             #f5f5f5 !default;
+
+$panel-default-text:          $gray-dark !default;
+$panel-default-border:        #ddd !default;
+$panel-default-heading-bg:    #f5f5f5 !default;
+
+$panel-primary-text:          #fff !default;
+$panel-primary-border:        $brand-primary !default;
+$panel-primary-heading-bg:    $brand-primary !default;
+
+$panel-success-text:          $state-success-text !default;
+$panel-success-border:        $state-success-border !default;
+$panel-success-heading-bg:    $state-success-bg !default;
+
+$panel-info-text:             $state-info-text !default;
+$panel-info-border:           $state-info-border !default;
+$panel-info-heading-bg:       $state-info-bg !default;
+
+$panel-warning-text:          $state-warning-text !default;
+$panel-warning-border:        $state-warning-border !default;
+$panel-warning-heading-bg:    $state-warning-bg !default;
+
+$panel-danger-text:           $state-danger-text !default;
+$panel-danger-border:         $state-danger-border !default;
+$panel-danger-heading-bg:     $state-danger-bg !default;
+
+
+//== Thumbnails
+//
+//##
+
+//** Padding around the thumbnail image
+$thumbnail-padding:           4px !default;
+//** Thumbnail background color
+$thumbnail-bg:                $body-bg !default;
+//** Thumbnail border color
+$thumbnail-border:            #ddd !default;
+//** Thumbnail border radius
+$thumbnail-border-radius:     $border-radius-base !default;
+
+//** Custom text color for thumbnail captions
+$thumbnail-caption-color:     $text-color !default;
+//** Padding around the thumbnail caption
+$thumbnail-caption-padding:   9px !default;
+
+
+//== Wells
+//
+//##
+
+$well-bg:                     #f5f5f5 !default;
+$well-border:                 darken($well-bg, 7%) !default;
+
+
+//== Badges
+//
+//##
+
+$badge-color:                 #fff !default;
+//** Linked badge text color on hover
+$badge-link-hover-color:      #fff !default;
+$badge-bg:                    $gray-light !default;
+
+//** Badge text color in active nav link
+$badge-active-color:          $link-color !default;
+//** Badge background color in active nav link
+$badge-active-bg:             #fff !default;
+
+$badge-font-weight:           bold !default;
+$badge-line-height:           1 !default;
+$badge-border-radius:         10px !default;
+
+
+//== Breadcrumbs
+//
+//##
+
+$breadcrumb-padding-vertical:   8px !default;
+$breadcrumb-padding-horizontal: 15px !default;
+//** Breadcrumb background color
+$breadcrumb-bg:                 #f5f5f5 !default;
+//** Breadcrumb text color
+$breadcrumb-color:              #ccc !default;
+//** Text color of current page in the breadcrumb
+$breadcrumb-active-color:       $gray-light !default;
+//** Textual separator for between breadcrumb elements
+$breadcrumb-separator:          "/" !default;
+
+
+//== Carousel
+//
+//##
+
+$carousel-text-shadow:                        none !default;
+
+$carousel-control-color:                      #333 !default;
+$carousel-control-width:                      25% !default;
+$carousel-control-opacity:                    1 !default;
+$carousel-control-font-size:                  20px !default;
+
+$carousel-indicator-active-bg:                #333 !default;
+$carousel-indicator-border-color:             #333 !default;
+
+$carousel-caption-color:                      #333 !default;
+
+
+//== Close
+//
+//##
+
+$close-font-weight:           bold !default;
+$close-color:                 #000 !default;
+$close-text-shadow:           0 1px 0 #fff !default;
+
+
+//== Code
+//
+//##
+
+$code-color:                  #c7254e !default;
+$code-bg:                     #f9f2f4 !default;
+
+$kbd-color:                   #fff !default;
+$kbd-bg:                      #333 !default;
+
+$pre-bg:                      #f5f5f5 !default;
+$pre-color:                   $gray-dark !default;
+$pre-border-color:            #ccc !default;
+$pre-scrollable-max-height:   340px !default;
+
+
+//== Type
+//
+//##
+
+//** Horizontal offset for forms and lists.
+$component-offset-horizontal: 180px !default;
+//** Text muted color
+$text-muted:                  $gray-light !default;
+//** Abbreviations and acronyms border color
+$abbr-border-color:           $gray-light !default;
+//** Headings small color
+$headings-small-color:        $gray-light !default;
+//** Blockquote small color
+$blockquote-small-color:      $gray-light !default;
+//** Blockquote font size
+$blockquote-font-size:        ($font-size-base * 1.25) !default;
+//** Blockquote border color
+$blockquote-border-color:     $gray-lighter !default;
+//** Page header border color
+$page-header-border-color:    $gray-lighter !default;
+//** Width of horizontal description list titles
+$dl-horizontal-offset:        $component-offset-horizontal !default;
+//** Point at which .dl-horizontal becomes horizontal
+$dl-horizontal-breakpoint:    $grid-float-breakpoint !default;
+//** Horizontal line color.
+$hr-border:                   $gray-lighter !default;
diff --git a/modules/frontend/public/stylesheets/_font-awesome-custom.scss b/modules/frontend/public/stylesheets/_font-awesome-custom.scss
new file mode 100644
index 0000000..16670a5
--- /dev/null
+++ b/modules/frontend/public/stylesheets/_font-awesome-custom.scss
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '~font-awesome/scss/variables';
+
+$fa-font-path: '~font-awesome/fonts';
+
+@import '~font-awesome/scss/mixins';
+@import '~font-awesome/scss/path';
+@import '~font-awesome/scss/core';
+@import '~font-awesome/scss/larger';
+@import '~font-awesome/scss/fixed-width';
+@import '~font-awesome/scss/list';
+@import '~font-awesome/scss/bordered-pulled';
+@import '~font-awesome/scss/animated';
+@import '~font-awesome/scss/rotated-flipped';
+@import '~font-awesome/scss/stacked';
+@import '~font-awesome/scss/icons';
+
+.fa {
+  cursor: pointer;
+}
+
+.icon-help {
+  @extend .fa;
+  @extend .fa-question-circle-o;
+
+  cursor: default;
+}
+
+.icon-confirm {
+  @extend .fa;
+  @extend .fa-question-circle-o;
+
+  cursor: default;
+}
+
+.icon-note {
+  @extend .fa;
+  @extend .fa-info-circle;
+
+  cursor: default;
+}
+
+.icon-danger {
+  @extend .fa;
+  @extend .fa-exclamation-triangle;
+
+  cursor: default;
+}
+
+.icon-success {
+  @extend .fa;
+  @extend .fa-check-circle-o;
+
+  cursor: default;
+}
+
+.icon-user {
+  @extend .fa;
+  @extend .fa-user-o;
+
+  cursor: default;
+}
+
+.icon-admin {
+  @extend .fa;
+  @extend .fa-user-secret;
+
+  cursor: default;
+}
+
+.icon-datepicker-left {
+  @extend .fa;
+  @extend .fa-chevron-left;
+
+  margin: 0;
+}
+
+.icon-datepicker-right {
+  @extend .fa;
+  @extend .fa-chevron-right;
+  
+  margin: 0;
+}
+
+.icon-cluster {
+  @extend .fa;
+  @extend .fa-sitemap;
+}
diff --git a/modules/frontend/public/stylesheets/blocks/error.scss b/modules/frontend/public/stylesheets/blocks/error.scss
new file mode 100644
index 0000000..4e16989
--- /dev/null
+++ b/modules/frontend/public/stylesheets/blocks/error.scss
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.error-page {
+    text-align: center;
+    min-height: 300px;
+
+    &__title {
+        margin-top: 150px;
+        font-weight: 200;
+    }
+
+    &__description {
+        margin-top: 30px;
+        font-weight: 500;
+    }
+}
diff --git a/modules/frontend/public/stylesheets/form-field.scss b/modules/frontend/public/stylesheets/form-field.scss
new file mode 100644
index 0000000..ae33d75
--- /dev/null
+++ b/modules/frontend/public/stylesheets/form-field.scss
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.details-row,
+.settings-row {
+    &_small-label {
+        @media (min-width: $screen-xs-min) {
+            .ignite-form-field__label {
+                @include make-xs-column(4);
+            }
+
+            .ignite-form-field__control {
+                @include make-xs-column(8);
+            }
+        }
+
+        @media (min-width: $screen-sm-min) {
+            .ignite-form-field__label {
+                @include make-sm-column(2);
+            }
+
+            .ignite-form-field__control {
+                @include make-sm-column(10);
+            }
+        }
+
+        @media (min-width: $screen-md-min) {
+            .ignite-form-field__label {
+                @include make-md-column(2);
+            }
+
+            .ignite-form-field__control {
+                @include make-md-column(10);
+            }
+        }
+
+        @media (min-width: $screen-lg-min) {
+            .ignite-form-field__label {
+                @include make-lg-column(2);
+            }
+
+            .ignite-form-field__control {
+                @include make-lg-column(10);
+            }
+        }
+    }
+
+    .ignite-form-field__label,
+    .ignite-form-field__control {
+        display: inline-block;
+        vertical-align: middle;
+        float: none;
+    }
+}
+
+@media (min-width: $screen-xs-min) {
+    .ignite-form-field__label {
+        @include make-xs-column(4);
+    }
+
+    .ignite-form-field__control {
+        @include make-xs-column(8);
+    }
+}
+
+@media (min-width: $screen-sm-min) {
+    .ignite-form-field__label {
+        @include make-sm-column(4);
+    }
+
+    .ignite-form-field__control {
+        @include make-sm-column(8);
+    }
+}
+
+@media (min-width: $screen-md-min) {
+    .ignite-form-field__label {
+        @include make-md-column(4);
+    }
+
+    .ignite-form-field__control {
+        @include make-md-column(8);
+    }
+}
+
+@media (min-width: $screen-lg-min) {
+    .ignite-form-field__label {
+        @include make-lg-column(4);
+    }
+
+    .ignite-form-field__control {
+        @include make-lg-column(8);
+    }
+}
+
+.ignite-form-field {
+    &__btn {
+        overflow: hidden;
+
+        border-top-left-radius: 0;
+        border-bottom-left-radius: 0;
+
+        &.btn {
+            float: right;
+            margin-right: 0;
+
+            line-height: 20px;
+        }
+
+        input {
+            position: absolute;
+            left: 100px;
+        }
+
+        input:checked + span {
+            color: $brand-info;
+        }
+    }
+
+    &__btn ~ &__btn {
+        border-right: 0;
+        border-top-right-radius: 0;
+        border-bottom-right-radius: 0;  
+    }
+
+    &__btn ~ .input-tip input {
+        border-right: 0;
+        border-top-right-radius: 0;
+        border-bottom-right-radius: 0;  
+    }
+}
diff --git a/modules/frontend/public/stylesheets/style.scss b/modules/frontend/public/stylesheets/style.scss
new file mode 100644
index 0000000..9414615
--- /dev/null
+++ b/modules/frontend/public/stylesheets/style.scss
@@ -0,0 +1,2171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import "./font-awesome-custom";
+@import "./bootstrap-custom";
+@import "./variables";
+@import "~roboto-font/css/fonts.css";
+@import "./blocks/error";
+@import "./form-field";
+
+:root {
+    --sans-serif-font: Roboto;
+    --serif-font: Roboto_slab;
+}
+
+body {
+    overflow-y: scroll !important;
+}
+
+.flex-full-height {
+    display: flex;
+    flex-direction: column;
+    flex: 1 0 auto;
+}
+
+.container--responsive {
+    padding: 0 30px;
+
+    .gridster-wrapper {
+        margin: -20px !important;
+    }
+}
+
+hr {
+    margin: 20px 0;
+}
+
+.theme-line a.active {
+    font-weight: bold;
+    font-size: 1.1em;
+}
+
+.theme-line a:focus {
+    text-decoration: underline;
+    outline: none;
+}
+
+.border-left {
+    box-shadow: 1px 0 0 0 $gray-lighter inset;
+}
+
+.border-right {
+    box-shadow: 1px 0 0 0 $gray-lighter;
+}
+
+.theme-line .docs-header h1 {
+    color: $ignite-header-color;
+    margin-top: 0;
+    font-size: 22px;
+}
+
+.table.table-vertical-middle tbody > tr > td {
+    vertical-align: middle;
+}
+
+.table .ui-grid-settings {
+    float: left;
+    padding-right: 10px;
+}
+
+
+
+.theme-line .select {
+    li a.active {
+        color: $dropdown-link-active-color;
+    }
+
+    li a:hover {
+        color: $dropdown-link-hover-color;
+    }
+}
+
+.theme-line .select,
+.theme-line .typeahead {
+    .active {
+        font-size: 1em;
+        background-color: $gray-lighter;
+    }
+}
+
+.theme-line button.form-control.placeholder {
+    color: $input-color-placeholder;
+}
+
+.tooltip {
+    word-wrap: break-word;
+}
+
+.theme-line ul.dropdown-menu {
+    min-width: 0;
+    max-width: 280px;
+    max-height: 20em;
+    overflow: auto;
+    overflow-x: hidden;
+    outline-style: none;
+    margin-top: 0;
+
+    li > a {
+        display: block;
+
+        padding: 3px 10px;
+
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+
+        i {
+            float: right;
+            color: $brand-primary;
+            background-color: transparent;
+            line-height: $line-height-base;
+            margin-left: 5px;
+            margin-right: 0;
+        }
+    }
+
+    li > div {
+        display: block;
+        overflow: hidden;
+
+        i {
+            float: right;
+            color: $text-color;
+            background-color: transparent;
+            line-height: $line-height-base;
+            margin: 0 10px 0 0;
+            padding: 6px 0;
+        }
+
+        div {
+            overflow: hidden;
+            text-overflow: ellipsis;
+        }
+    }
+
+    // Hover/Focus state
+    li > div a {
+        float: left;
+        display: block;
+        width: 100%;
+        padding: 3px 10px;
+        color: $dropdown-link-color;
+
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+
+        &:hover,
+        &:focus {
+            text-decoration: none;
+            color: $dropdown-link-hover-color;
+            background-color: $dropdown-link-hover-bg;
+        }
+    }
+
+    // Active state
+    .active > div a {
+        cursor: default;
+        pointer-events: none;
+
+        &,
+        &:hover,
+        &:focus {
+            color: $dropdown-link-active-color;
+            text-decoration: none;
+            outline: 0;
+            background-color: $dropdown-link-active-bg;
+        }
+    }
+
+    li.divider {
+        margin: 3px 0;
+    }
+}
+
+
+.theme-line .suggest {
+    padding: 5px;
+    display: inline-block;
+    font-size: 12px;
+}
+
+.nav > li {
+    > a {
+        color: $navbar-default-link-color;
+
+        &:hover,
+        &:focus {
+            color: $link-hover-color;
+        }
+
+        &.active {
+            color: $link-color;
+        }
+    }
+
+    &.disabled > a {
+        label:hover, label:focus, i:hover, i:focus {
+            cursor: default;
+        }
+    }
+}
+
+.body-overlap .main-content {
+    margin-top: 30px;
+}
+
+.body-box .main-content,
+.body-overlap .main-content {
+    padding: 30px;
+    box-shadow: 0 0 0 1px $ignite-border-color;
+    background-color: $ignite-background-color;
+}
+
+body {
+    font-weight: 400;
+    background-color: $ignite-new-background-color;
+}
+
+h1, h2, h3, h4, h5, h6 {
+    font-weight: 700;
+    margin-bottom: 10px;
+}
+
+/* Modal */
+.login-header {
+    margin-top: 0;
+    margin-bottom: 20px;
+    font-size: 2em;
+}
+
+body {
+    overflow-x: hidden;
+    display: flex;
+    flex-direction: column;
+}
+
+:root {
+    --page-side-padding: 30px;
+}
+
+.wrapper {
+    --header-height: 62px;
+
+    min-height: 100vh;
+    display: grid;
+    grid-template-columns: auto 1fr;
+    grid-template-rows: auto var(--header-height) 1fr;
+    grid-template-areas: 'notifications notifications' 'header header' 'sidebar content';
+
+    permanent-notifications {
+        grid-area: notifications;
+    }
+
+    web-console-header {
+        grid-area: header;
+        position: -webkit-sticky;
+        position: sticky;
+        top: 0;
+        z-index: 10;
+        transform: translateZ(1px);
+    }
+
+    web-console-sidebar {
+        grid-area: sidebar;
+        transform: translateZ(1px);
+    }
+
+    &>.content {
+        grid-area: content;
+        padding-left: var(--page-side-padding);
+        padding-right: var(--page-side-padding);
+        padding-bottom: var(--page-side-padding);
+        display: flex;
+        flex-direction: column;
+
+        &>ui-view {
+            display: flex;
+            flex-direction: column;
+            flex: 1;
+        }
+    }
+
+    &.wrapper-public {
+        grid-template-columns: 1fr;
+        grid-template-rows: auto var(--header-height) 1fr auto;
+        grid-template-areas: 'notifications' 'header' 'content' 'footer';
+    }
+}
+
+.public-page {
+    margin-left: auto;
+    margin-right: auto;
+    flex: 1 0 auto;
+    width: 100%;
+    max-width: 530px;
+
+    .public-page__title {
+        font-size: 38px;
+        font-weight: 300;
+        margin: 30px 0 30px;
+    }
+}
+
+.details-row {
+    padding: 0 5px;
+}
+
+.details-row, .settings-row {
+    display: block;
+    margin: 10px 0;
+
+    [class*="col-"] {
+        display: inline-block;
+        vertical-align: middle;
+        float: none;
+    }
+
+    input[type="checkbox"] {
+        line-height: 20px;
+        margin-right: 5px;
+    }
+
+    .checkbox label {
+        line-height: 20px !important;
+        vertical-align: middle;
+    }
+}
+
+.use-cache {
+    display: flex;
+}
+
+.group-section {
+    margin-top: 20px;
+}
+
+.details-row:first-child {
+    margin-top: 0;
+
+    .group-section {
+        margin-top: 10px;
+    }
+}
+
+.details-row:last-child {
+    margin-bottom: 0;
+}
+
+.settings-row:first-child {
+    margin-top: 0;
+
+    .group-section {
+        margin-top: 0;
+    }
+}
+
+.settings-row:last-child {
+    margin-bottom: 0;
+}
+
+button, .btn {
+    margin-right: 5px;
+}
+
+i.btn {
+    margin-right: 0;
+}
+
+.btn {
+    padding: 3px 6px;
+
+    :focus {
+        //outline: none;
+        //border: 1px solid $btn-default-border;
+    }
+}
+
+.btn-group.pull-right {
+    margin-right: 0;
+}
+
+.btn-group {
+    margin-right: 5px;
+
+    > button, a.btn {
+        margin-right: 0;
+    }
+
+    button.btn + .btn {
+        margin-left: 0;
+    }
+
+    button.btn[disabled="disabled"] {
+        i {
+            cursor: not-allowed;
+        }
+    }
+
+    > .btn + .dropdown-toggle {
+        margin-right: 0;
+        padding: 3px 6px;
+        border-left-width: 0;
+    }
+}
+
+h1,
+h2,
+h3 {
+    font-weight: normal;
+    /* Makes the vertical size of the text the same for all fonts. */
+    line-height: 1;
+}
+
+h3 {
+    font-size: 1.2em;
+    margin-top: 0;
+    margin-bottom: 1.5em;
+}
+
+.base-control {
+    text-align: left;
+    padding: 3px 3px;
+    height: $input-height;
+}
+
+.sql-name-input {
+    @extend .form-control;
+
+    width: auto;
+}
+
+.form-control {
+    @extend .base-control;
+
+    display: inline-block;
+
+    button {
+        text-align: left;
+    }
+}
+
+button.form-control {
+    display: block;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+}
+
+.theme-line .notebook-header {
+    border-color: $gray-lighter;
+
+    button:last-child {
+        margin-right: 0;
+    }
+
+    h1 {
+        padding: 0;
+        margin: 0;
+
+        height: 40px;
+
+        label {
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            height: 24px;
+        }
+
+        .btn-group {
+            margin-top: -5px;
+            margin-left: 5px;
+        }
+
+        > i.btn {
+            float: right;
+            line-height: 30px;
+        }
+
+        input {
+            font-size: 22px;
+            height: 35px;
+        }
+
+        a.dropdown-toggle {
+            font-size: $font-size-base;
+            margin-right: 5px;
+        }
+    }
+}
+
+.theme-line .paragraphs {
+    .panel-group .panel-paragraph + .panel-paragraph {
+        margin-top: 30px;
+    }
+
+    .btn-group:last-of-type {
+        margin-right: 0;
+    }
+
+    .sql-editor {
+        padding: 5px 0;
+
+        .ace_gutter-cell, .ace_folding-enabled > .ace_gutter-cell {
+            padding-right: 5px;
+        }
+    }
+
+    table thead {
+        background-color: white;
+    }
+
+    .wrong-caches-filter {
+        text-align: center;
+        color: $ignite-placeholder-color;
+        height: 65px;
+        line-height: 65px;
+    }
+
+    .panel-collapse {
+        border-top: 1px solid $ignite-border-color;
+    }
+
+    .btn-ignite-group {
+        padding: 0;
+        border: none;
+        margin-right: 0;
+        background: transparent;
+    }
+
+    .sql-controls {
+        display: flex;
+        justify-content: space-between;
+        border-top: 1px solid #ddd;
+
+        & > div {
+            display: flex;
+            padding: 10px;
+            align-items: flex-start;
+
+            &:nth-child(2) {
+                flex: 1;
+                justify-content: flex-end;
+            }
+
+            &:last-child {
+                flex-direction: column;
+                flex-basis: 25%;
+            }
+        }
+
+        button + button {
+            margin-left: 20px;
+        }
+
+        .form-field--inline + .form-field--inline {
+            margin-left: 20px;
+        }
+    }
+
+    .sql-result {
+        border-top: 1px solid $ignite-border-color;
+
+        .error {
+            padding: 10px 10px;
+
+            text-align: left;
+        }
+
+        .empty {
+            padding: 10px 10px;
+
+            text-align: center;
+            color: $ignite-placeholder-color;
+        }
+
+        .total {
+            padding: 10px 10px;
+
+            input[type="checkbox"] {
+                line-height: 20px;
+                margin-right: 5px;
+            }
+
+            label {
+                line-height: 20px !important;
+                vertical-align: middle;
+            }
+        }
+
+        .table {
+            margin: 0
+        }
+
+        .chart {
+            margin: 0
+        }
+
+        .footer {
+            border-top: 1px solid $ignite-border-color;
+
+            padding: 5px 10px;
+        }
+
+        grid-column-selector {
+            margin-right: 5px;
+
+            .btn-ignite {
+                padding: 5px 0;
+                box-shadow: none !important;
+
+                .fa {
+                    font-size: 14px;
+                }
+            }
+        }
+    }
+}
+
+.theme-line .panel-heading {
+    margin: 0;
+    cursor: pointer;
+    font-size: $font-size-large;
+    line-height: 24px;
+
+    .btn-group {
+        vertical-align: top;
+        margin-left: 10px;
+
+        i { line-height: 18px; }
+    }
+
+    > i {
+        vertical-align: top;
+        line-height: 26px;
+        height: 26px;
+    }
+
+    .fa {
+        line-height: 26px;
+    }
+
+    .fa-floppy-o {
+        float: right;
+    }
+
+    .fa-chevron-circle-right, .fa-chevron-circle-down {
+        font-size: $font-size-base;
+        color: inherit;
+        float: left;
+    }
+
+    .fa-undo {
+        padding: 1px 6px;
+
+        font-size: 16px;
+    }
+
+    .fa-undo:hover {
+        padding: 0 5px;
+
+        border-radius: 5px;
+        border: thin dotted $ignite-darck-border-color;
+    }
+}
+
+.theme-line .panel-heading:hover {
+    text-decoration: underline;
+}
+
+.theme-line .panel-body {
+    padding: 20px;
+}
+
+.theme-line .panel-collapse {
+    margin: 0;
+}
+
+.theme-line table.links {
+    table-layout: fixed;
+    border-collapse: collapse;
+
+    width: 100%;
+
+    label.placeholder {
+        text-align: center;
+        color: $ignite-placeholder-color;
+        width: 100%;
+    }
+
+    input[type="text"] {
+        font-weight: normal;
+    }
+
+    input[type="radio"] {
+        margin-left: 1px;
+        margin-right: 5px;
+    }
+
+    tbody {
+        border-left: 10px solid transparent;
+    }
+
+    tbody td:first-child {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+    }
+
+    tfoot > tr > td {
+        padding: 0;
+
+        .pagination {
+            margin: 10px 0;
+
+            > .active > a {
+                border-color: $table-border-color;
+                background-color: $gray-lighter;
+            }
+        }
+    }
+}
+
+.theme-line table.links-edit {
+    @extend table.links;
+
+    margin-top: 0;
+    margin-bottom: 5px;
+
+    label {
+        line-height: $input-height;
+    }
+
+    td {
+        padding-left: 0;
+    }
+}
+
+.theme-line table.links-edit-sub {
+    @extend table.links-edit;
+
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+.theme-line table.links-edit-details {
+    @extend table.links;
+
+    margin-bottom: 10px;
+
+    label {
+        line-height: $input-height;
+        color: $ignite-header-color;
+    }
+
+    td {
+        padding: 0;
+
+        .input-tip {
+            padding: 0;
+        }
+    }
+}
+
+.theme-line table.admin {
+    tr:hover {
+        cursor: default;
+    }
+
+    thead {
+        .pagination {
+            margin: 0;
+        }
+    }
+
+    thead > tr th.header {
+        padding: 0 0 10px;
+
+        div {
+            padding: 0;
+        }
+
+        input[type="text"] {
+            font-weight: normal;
+        }
+    }
+
+    margin-bottom: 10px;
+
+    label {
+        line-height: $input-height;
+        color: $ignite-header-color;
+    }
+
+    thead > tr th, td {
+        padding: 10px 10px;
+
+        .input-tip {
+            padding: 0;
+        }
+    }
+
+    tfoot > tr > td {
+        padding: 0;
+    }
+
+    .pagination {
+        margin: 10px 0;
+        font-weight: normal;
+
+        > .active > a {
+            border-color: $table-border-color;
+            background-color: $gray-lighter;
+        }
+    }
+}
+
+.admin-summary {
+    padding-bottom: 10px;
+}
+
+.scrollable-y {
+    overflow-x: hidden;
+    overflow-y: auto;
+}
+
+.theme-line table.metadata {
+    margin-bottom: 10px;
+
+    tr:hover {
+        cursor: default;
+    }
+
+    thead > tr {
+        label {
+            font-weight: bold;
+        }
+
+        input[type="checkbox"] {
+            cursor: pointer;
+        }
+    }
+
+    thead > tr th.header {
+        padding: 0 0 10px;
+
+        .pull-right {
+            padding: 0;
+        }
+
+        input[type="checkbox"] {
+            cursor: pointer;
+        }
+
+        input[type="text"] {
+            font-weight: normal;
+        }
+    }
+
+    > thead > tr > th {
+        padding: 5px 0 5px 5px !important;
+    }
+
+    tbody > tr > td {
+        padding: 0;
+    }
+}
+
+.td-ellipsis {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.table-modal-striped {
+    width: 100%;
+
+    > tbody > tr {
+        border-bottom: 2px solid $ignite-border-color;
+
+        input[type="checkbox"] {
+            cursor: pointer;
+        }
+    }
+
+    > tbody > tr > td {
+        padding: 5px 0 5px 5px !important;
+    }
+}
+
+.theme-line table.sql-results {
+    margin: 0;
+
+    td {
+        padding: 3px 6px;
+    }
+
+    > thead > tr > td {
+        padding: 3px 0;
+    }
+
+    thead > tr > th {
+        padding: 3px 6px;
+
+        line-height: $input-height;
+    }
+
+    tfoot > tr > td {
+        padding: 0;
+
+        .pagination {
+            margin: 10px 0 0 0;
+
+            > .active > a {
+                border-color: $table-border-color;
+                background-color: $gray-lighter;
+            }
+        }
+    }
+}
+
+.affix {
+    z-index: 910;
+    background-color: white;
+
+    hr {
+        margin: 0;
+    }
+}
+
+.affix.padding-top-dflt {
+    hr {
+        margin-top: 10px;
+    }
+}
+
+.affix + .bs-affix-fix {
+    height: 78px;
+}
+
+.panel-details {
+    margin-top: 5px;
+    padding: 10px 5px;
+
+    border-radius: 5px;
+    border: thin dotted $ignite-border-color;
+}
+
+.panel-details-noborder {
+    margin-top: 5px;
+    padding: 10px 5px;
+}
+
+.group {
+    border-radius: 5px;
+    border: thin dotted $ignite-border-color;
+
+    text-align: left;
+
+    hr {
+        margin: 7px 0;
+    }
+}
+
+.group-legend {
+    margin: -10px 5px 0 10px;
+    overflow: visible;
+    position: relative;
+
+    label {
+        padding: 0 5px;
+        background: white;
+    }
+}
+
+.group-legend-btn {
+    background: white;
+    float: right;
+    line-height: 20px;
+    padding: 0 5px 0 5px;
+}
+
+.group-content {
+    margin: 10px;
+
+    table {
+        width: 100%;
+    }
+}
+
+.group-content-empty {
+    color: $input-color-placeholder;
+
+    padding: 10px 0;
+    position: relative;
+
+    text-align: center;
+}
+
+.content-not-available {
+    min-height: 28px;
+
+    margin-right: 20px;
+
+    border-radius: 5px;
+    border: thin dotted $ignite-border-color;
+
+    padding: 0;
+
+    color: $input-color-placeholder;
+    display: table;
+    width: 100%;
+    height: 26px;
+
+    label {
+        display: table-cell;
+        text-align: center;
+        vertical-align: middle;
+    }
+}
+
+.tooltip > .tooltip-inner {
+    text-align: left;
+    border: solid 1px #ccc;
+}
+
+.popover-footer {
+    margin: 0; // reset heading margin
+    padding: 8px 14px;
+    font-size: $font-size-base;
+    color: $input-color-placeholder;
+    background-color: $popover-title-bg;
+    border-top: 1px solid darken($popover-title-bg, 5%);
+    border-radius: 0 0 ($border-radius-large - 1) ($border-radius-large - 1);
+}
+
+.popover-content {
+    padding: 5px;
+
+    button {
+        margin-left: 5px;
+    }
+}
+
+.popover:focus {
+    outline: none;
+    border: 1px solid $btn-default-border;
+}
+
+.theme-line .popover.settings {
+    .close {
+        position: absolute;
+        top: 5px;
+        right: 5px;
+    }
+}
+
+.theme-line .popover.cache-metadata {
+    @extend .popover.settings;
+
+    position: absolute;
+    z-index: 1030;
+    min-width: 305px;
+    max-width: 335px;
+
+    treecontrol li {
+        line-height: 16px;
+    }
+
+    treeitem ul {
+        margin-top: -2px;
+        margin-bottom: 2px;
+    }
+
+    .node-display {
+        position: relative;
+        top: 0;
+
+        max-width: 280px;
+
+        text-overflow: ellipsis;
+        line-height: 16px;
+
+        overflow: hidden;
+    }
+
+    .popover-title {
+        color: black;
+
+        line-height: 27px;
+
+        padding: 3px 5px 3px 10px;
+
+        white-space: nowrap;
+        overflow: hidden;
+        -o-text-overflow: ellipsis;
+        text-overflow: ellipsis;
+
+        .close {
+            float: right;
+            top: 0;
+            right: 0;
+            position: relative;
+            margin-left: 10px;
+            line-height: 27px;
+        }
+    }
+
+    > .popover-content {
+        overflow: auto;
+
+        white-space: nowrap;
+
+        min-height: 400px;
+        max-height: 400px;
+
+        .content-empty {
+            display: block;
+            text-align: center;
+            line-height: 380px;
+
+            color: $input-color-placeholder;
+        }
+    }
+
+    .clickable { cursor: pointer; }
+}
+
+
+.theme-line .summary {
+    .actions-note {
+        i {
+            margin-right: 5px;
+        }
+
+        margin: 15px 0;
+    }
+}
+
+.popover.validation-error {
+    white-space: pre-wrap;
+    width: auto !important;
+    max-width: 400px !important;
+    color: $brand-primary;
+    background: white;
+    border: 1px solid $brand-primary;
+
+    &.right > .arrow {
+        border-right-color: $brand-primary;
+    }
+
+    .close {
+        vertical-align: middle;
+    }
+}
+
+label {
+    font-weight: normal;
+    margin-bottom: 0;
+}
+
+.form-horizontal .checkbox {
+    padding-top: 0;
+    min-height: 0;
+}
+
+.input-tip {
+    display: block;
+    overflow: hidden;
+    position: relative;
+}
+
+.labelHeader {
+    font-weight: bold;
+    text-transform: capitalize;
+}
+
+.labelField {
+    float: left;
+    margin-right: 5px;
+}
+
+.labelFormField {
+    float: left;
+    line-height: $input-height;
+}
+
+.labelLogin {
+    margin-right: 10px;
+}
+
+.form-horizontal .form-group {
+    margin: 0;
+}
+
+.form-horizontal .has-feedback .form-control-feedback {
+    right: 0;
+}
+
+.tipField {
+    float: right;
+    line-height: $input-height;
+    margin-left: 5px;
+}
+
+.tipLabel {
+    font-size: $font-size-base;
+    margin-left: 5px;
+}
+
+.fieldSep {
+    float: right;
+    line-height: $input-height;
+    margin: 0 5px;
+}
+
+
+.fa-cursor-default {
+    cursor: default !important;
+}
+
+.fa-remove {
+    color: $brand-primary;
+}
+
+.fa-chevron-circle-down {
+    color: $ignite-brand-success;
+    margin-right: 5px;
+}
+
+.fa-chevron-circle-right {
+    color: $ignite-brand-success;
+    margin-right: 5px;
+}
+
+.required:after {
+    color: $brand-primary;
+    content: ' *';
+    display: inline;
+}
+
+.blank {
+    visibility: hidden;
+}
+
+.alert {
+    outline: 0;
+    padding: 10px;
+    position: fixed;
+    z-index: 1050;
+    margin: 20px;
+    max-width: 700px;
+
+    &.top-right {
+        top: 60px;
+        right: 0;
+
+        .close {
+            padding-left: 10px;
+        }
+    }
+
+    .alert-icon {
+        padding-right: 10px;
+        font-size: 16px;
+    }
+
+    .alert-title {
+        color: $text-color;
+    }
+
+    .close {
+        margin-right: 0;
+        line-height: 19px;
+    }
+}
+
+input[type="number"]::-webkit-outer-spin-button,
+input[type="number"]::-webkit-inner-spin-button {
+    -webkit-appearance: none;
+    margin: 0;
+}
+
+input[type="number"] {
+    -moz-appearance: textfield;
+}
+
+input.ng-dirty.ng-invalid, button.ng-dirty.ng-invalid {
+    border-color: $ignite-invalid-color;
+
+    :focus {
+        border-color: $ignite-invalid-color;
+    }
+}
+
+.form-control-feedback {
+    display: inline-block;
+    color: $brand-primary;
+    line-height: $input-height;
+    pointer-events: initial;
+}
+
+.theme-line .nav-tabs > li > a {
+    padding: 5px 5px;
+    color: $ignite-header-color;
+}
+
+a {
+    cursor: pointer;
+}
+
+.st-sort-ascent:after {
+    content: '\25B2';
+}
+
+.st-sort-descent:after {
+    content: '\25BC';
+}
+
+th[st-sort] {
+    cursor: pointer;
+}
+
+.panel {
+    margin-bottom: 0;
+}
+
+.panel-group {
+    margin-bottom: 0;
+}
+
+.panel-group .panel + .panel {
+    margin-top: 20px;
+}
+
+.section {
+    margin-top: 20px;
+}
+
+.section-top {
+    width: 100%;
+    margin-top: 10px;
+    margin-bottom: 20px;
+}
+
+.advanced-options {
+    @extend .section;
+    margin-bottom: 20px;
+
+    i {
+        font-size: 16px;
+    }
+}
+
+.modal-advanced-options {
+    @extend .advanced-options;
+    margin-top: 10px;
+    margin-bottom: 10px;
+}
+
+.margin-left-dflt {
+    margin-left: 10px;
+}
+
+.margin-top-dflt {
+    margin-top: 10px;
+}
+
+.margin-top-dflt-2x {
+    margin-top: 20px;
+}
+
+.margin-bottom-dflt {
+    margin-bottom: 10px;
+}
+
+.margin-dflt {
+    margin-top: 10px;
+    margin-bottom: 10px;
+}
+
+.padding-top-dflt {
+    padding-top: 10px;
+}
+
+.padding-left-dflt {
+    padding-left: 10px;
+}
+
+.padding-bottom-dflt {
+    padding-bottom: 10px;
+}
+
+.padding-dflt {
+    padding-top: 10px;
+    padding-bottom: 10px;
+}
+
+.agent-download {
+    padding: 10px 10px 10px 20px;
+}
+
+// Hack to solve an issue where scrollbars aren't visible in Safari.
+// Safari seems to clip part of the scrollbar element. By giving the
+// element a background, we're telling Safari that it *really* needs to
+// paint the whole area. See https://github.com/ajaxorg/ace/issues/2872
+.ace_scrollbar-inner {
+    background-color: #FFF;
+    opacity: 0.01;
+
+    .ace_dark & {
+        background-color: #000;
+    }
+}
+
+.ace_content {
+    padding-left: 5px;
+}
+
+.ace_editor {
+    margin: 10px 5px 10px 0;
+
+    .ace_gutter {
+        background: transparent !important;
+        border: 1px $ignite-border-color;
+        border-right-style: solid;
+    }
+
+    .ace_gutter-cell, .ace_folding-enabled > .ace_gutter-cell {
+        padding-left: 0.65em;
+    }
+}
+
+.preview-highlight-1 {
+    position: absolute;
+    background-color: #f7faff;
+    z-index: 20;
+}
+
+.preview-highlight-2 {
+    position: absolute;
+    background-color: #f0f6ff;
+    z-index: 21;
+}
+
+.preview-highlight-3 {
+    position: absolute;
+    background-color: #e8f2ff;
+    z-index: 22;
+}
+
+.preview-highlight-4 {
+    position: absolute;
+    background-color: #e1eeff;
+    z-index: 23;
+}
+
+.preview-highlight-5 {
+    position: absolute;
+    background-color: #DAEAFF;
+    z-index: 24;
+}
+
+.preview-highlight-6 {
+    position: absolute;
+    background-color: #D2E5FF;
+    z-index: 25;
+}
+
+.preview-highlight-7 {
+    position: absolute;
+    background-color: #CBE1FF;
+    z-index: 26;
+}
+
+.preview-highlight-8 {
+    position: absolute;
+    background-color: #C3DDFF;
+    z-index: 27;
+}
+
+.preview-highlight-9 {
+    position: absolute;
+    background-color: #BCD9FF;
+    z-index: 28;
+}
+
+.preview-highlight-10 {
+    position: absolute;
+    background-color: #B5D5FF;
+    z-index: 29;
+}
+
+.preview-panel {
+    min-height: 28px;
+    position: relative;
+
+    margin-left: 20px;
+
+    border-radius: 5px;
+    border: thin dotted $ignite-border-color;
+
+    padding: 0;
+}
+
+.preview-legend {
+    top: -10px;
+    right: 20px;
+    position: absolute;
+    z-index: 2;
+
+    a {
+        color: $input-color-placeholder;
+        background-color: white;
+        margin-left: 5px;
+        font-size: 0.9em;
+    }
+
+    a + a {
+        margin-left: 10px
+    }
+
+    a.active {
+        color: $brand-primary;
+    }
+}
+
+.preview-content-empty {
+    color: #757575;
+    display: table;
+    width: 100%;
+    height: 26px;
+
+    label {
+        display: table-cell;
+        text-align: center;
+        vertical-align: middle;
+    }
+}
+
+.chart-settings-link {
+    margin-top: -2px;
+    padding-left: 10px;
+    line-height: $input-height;
+
+    label, button {
+        margin-left: 5px;
+        margin-right: 0;
+    }
+
+    button.select-manual-caret {
+        padding-right: 3px;
+
+        .caret { margin-left: 3px; }
+    }
+
+    a, i {
+        font-size: $font-size-base;
+        color: $link-color !important;
+        margin-right: 5px;
+    }
+
+    div {
+        margin-left: 20px;
+        display: inline-block;
+    }
+}
+
+.chart-settings {
+    margin: 10px 5px 5px 5px !important;
+}
+
+.chart-settings-columns-list {
+    border: 1px solid $ignite-border-color;
+    list-style: none;
+    margin-bottom: 10px;
+    min-height: 30px;
+    max-height: 200px;
+    padding: 5px;
+
+    overflow: auto;
+
+    & > li {
+        float: left;
+    }
+
+    li:nth-child(even) {
+        margin-right: 0;
+    }
+
+    .fa-close {
+        margin-left: 10px;
+    }
+}
+
+.btn-chart-column {
+    border-radius: 3px;
+    font-size: 12px;
+    margin: 3px 3px;
+    padding: 1px 5px;
+    line-height: 1.5;
+    cursor: default;
+}
+
+.btn-chart-column-movable {
+    @extend .btn-chart-column;
+    cursor: move;
+}
+
+.btn-chart-column-agg-fx {
+    border: 0;
+    margin: 0 0 0 10px;
+}
+
+.dw-loading {
+    min-height: 100px;
+}
+
+.dw-loading > .dw-loading-body > .dw-loading-text {
+    left: -50%;
+}
+
+.dw-loading.dw-loading-overlay {
+    z-index: 1030;
+}
+
+.modal {
+    .dw-loading.dw-loading-overlay {
+        z-index: 9999;
+    }
+
+    .dw-loading-body {
+        left: 10%;
+    }
+}
+
+.modal-backdrop {
+    background-color: rgba(0, 0, 0, 0.6) !important;
+}
+
+.am-fade-and-scale {
+    animation-duration: 0.3s;
+    animation-timing-function: ease-in-out;
+    animation-fill-mode: backwards;
+
+    &.ng-enter, &.am-fade-and-scale-add, &.ng-hide-remove, &.ng-move {
+        animation-name: fadeAndScaleIn;
+    }
+
+    &.ng-leave, &.am-fade-and-scale-remove, &.ng-hide, &.ng-move {
+        animation-name: fadeAndScaleOut;
+    }
+
+    &.ng-enter {
+        visibility: hidden;
+        animation-name: fadeAndScaleIn;
+        animation-play-state: paused;
+
+        &.ng-enter-active {
+            visibility: visible;
+            animation-play-state: running;
+        }
+    }
+
+    &.ng-leave {
+        animation-name: fadeAndScaleOut;
+        animation-play-state: paused;
+
+        &.ng-leave-active {
+            animation-play-state: running;
+        }
+    }
+
+
+}
+
+@keyframes fadeAndScaleIn {
+    from {
+        opacity: 0;
+        transform: scale(0.7);
+    }
+    to {
+        opacity: 1;
+    }
+}
+
+@keyframes fadeAndScaleOut {
+    from {
+        opacity: 1;
+    }
+    to {
+        opacity: 0;
+        transform: scale(0.7);
+    }
+}
+
+.panel-tip-container {
+    display: inline-block;
+}
+
+.panel-top-align {
+    label {
+        vertical-align: top !important;
+    }
+}
+
+button.dropdown-toggle {
+    margin-right: 5px;
+}
+
+button.select-toggle {
+    position: relative;
+    padding-right: 15px;
+}
+
+button.select-toggle::after {
+    content: "";
+    border-top: 0.3em solid;
+    border-right: 0.3em solid transparent;
+    border-left: 0.3em solid transparent;
+    position: absolute;
+    right: 10px;
+    top: 50%;
+    vertical-align: middle;
+}
+
+// Prevent scroll bars from being hidden for OS X.
+::-webkit-scrollbar {
+    -webkit-appearance: none;
+}
+
+::-webkit-scrollbar:vertical {
+    width: 10px;
+}
+
+::-webkit-scrollbar:horizontal {
+    height: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+    border-radius: 8px;
+    border: 2px solid white; /* should match background, can't be transparent */
+    background-color: rgba(0, 0, 0, .5);
+}
+
+::-webkit-scrollbar-track {
+    background-color: white;
+    border-radius: 8px;
+}
+
+treecontrol.tree-classic {
+    > ul > li {
+        padding: 0;
+    }
+
+    li {
+        padding-left: 15px;
+    }
+
+    li.tree-expanded i.tree-branch-head.fa, li.tree-collapsed i.tree-branch-head.fa, li.tree-leaf i.tree-branch-head.fa, .tree-label i.fa {
+        background: none no-repeat;
+        padding: 1px 5px 1px 1px;
+    }
+
+    li.tree-leaf i.tree-leaf-head {
+        background: none no-repeat !important;
+        padding: 0 !important;
+    }
+
+    li .tree-selected {
+        background-color: white;
+        font-weight: normal;
+    }
+
+    span {
+        margin-right: 10px;
+    }
+}
+
+.docs-content {
+    .affix {
+        border-bottom: 1px solid $gray-lighter;
+    }
+
+    min-height: 100px;
+}
+
+.carousel-caption {
+    position: relative;
+    left: auto;
+    right: auto;
+
+    margin-top: 10px;
+
+    h3 {
+        margin-bottom: 10px;
+    }
+}
+
+.carousel-control {
+    font-size: 20px;
+    z-index: 16;
+
+    // Toggles
+    .fa-chevron-left,.fa-chevron-right {
+        position: absolute;
+        bottom: 28px;
+        margin-top: -10px;
+        z-index: 16;
+        display: inline-block;
+        margin-left: -10px;
+    }
+
+    .fa-chevron-left {
+        left: 90%;
+        margin-left: -10px;
+    }
+
+    .fa-chevron-right {
+        right: 90%;
+        margin-right: -10px;
+    }
+}
+
+.carousel-control.left {
+    background-image: none;
+}
+
+.carousel-control.right {
+    background-image: none;
+}
+
+.getting-started-puzzle {
+    margin-left: 20px;
+}
+
+.getting-started {
+    min-height: 240px;
+    margin: 15px 15px 300px;
+
+    ul {
+        line-height: 20px;
+    }
+
+    [class*="col-"] {
+        align-self: flex-start !important;
+    }
+
+    .align-center {
+        justify-content: center !important;
+    }
+}
+
+.getting-started-demo {
+    color: $brand-info;
+}
+
+.home-panel {
+    border-radius: 5px;
+    border: thin dotted $panel-default-border;
+    background-color: $panel-default-heading-bg;
+
+    margin-top: 20px;
+    padding: 10px;
+}
+
+.home {
+    min-height: 880px;
+    padding: 20px;
+
+    @media(min-width: 992px) {
+        min-height: 450px;
+    }
+}
+
+.additional-filter {
+    input[type="checkbox"] {
+        position: absolute;
+        margin-top: 8px;
+    }
+
+    a {
+        font-weight: normal;
+        padding-left: 20px;
+        float: none;
+    }
+}
+
+.grid {
+    .ui-grid-header-cell .ui-grid-cell-contents {
+        text-align: center;
+
+        > span:not(.ui-grid-header-cell-label) {
+            position: absolute;
+            right: -3px;
+        }
+    }
+
+    .ui-grid-cell .ui-grid-cell-contents {
+        text-align: center;
+        white-space: pre;
+
+        > i.fa {
+            cursor: default;
+        }
+    }
+
+    .ui-grid-column-menu-button {
+        right: -3px;
+    }
+
+    .ui-grid-menu-button {
+        margin-top: -1px;
+    }
+
+    .ui-grid-column-menu-button-last-col {
+        margin-right: 0
+    }
+
+    .ui-grid-cell-actions {
+        line-height: 28px;
+    }
+
+    .no-rows {
+        .center-container {
+            background: white;
+
+            .centered > div {
+                display: inline-block;
+                padding: 10px;
+
+                opacity: 1;
+
+                background-color: #f5f5f5;
+                border-radius: 6px;
+                border: 1px solid $ignite-darck-border-color;
+            }
+        }
+    }
+}
+
+.cell-right .ui-grid-cell-contents {
+    text-align: right !important;
+}
+
+.cell-left .ui-grid-cell-contents {
+    text-align: left !important;
+}
+
+.grid.ui-grid {
+    border-left-width: 0;
+    border-right-width: 0;
+    border-bottom-width: 0;
+}
+
+html, body {
+    width: 100%;
+    min-height: 100vh;
+}
+
+.splash {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    top: 0;
+    opacity: 1;
+    background-color: white;
+    z-index: 99999;
+
+    .splash-wrapper {
+        display: inline-block;
+        vertical-align: middle;
+        position: relative;
+        width: 100%;
+    }
+
+    .splash-wellcome {
+        font-size: 18px;
+        margin: 20px 0;
+        text-align: center;
+    }
+}
+
+.splash:before {
+    content: '';
+    display: inline-block;
+    height: 100%;
+    vertical-align: middle;
+}
+
+.spinner {
+    margin: 0 auto;
+    width: 100px;
+    text-align: center;
+
+    > div {
+        width: 18px;
+        height: 18px;
+        margin: 0 5px;
+        border-radius: 100%;
+        display: inline-block;
+        -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
+        animation: sk-bouncedelay 1.4s infinite ease-in-out both;
+        background-color: $brand-primary;
+    }
+
+    .bounce1 {
+        -webkit-animation-delay: -0.32s;
+        animation-delay: -0.32s;
+    }
+
+    .bounce2 {
+        -webkit-animation-delay: -0.16s;
+        animation-delay: -0.16s;
+    }
+}
+
+@-webkit-keyframes sk-bouncedelay {
+    0%, 80%, 100% {
+        -webkit-transform: scale(0)
+    }
+    40% {
+        -webkit-transform: scale(1.0)
+    }
+}
+
+@keyframes sk-bouncedelay {
+    0%, 80%, 100% {
+        -webkit-transform: scale(0);
+        transform: scale(0);
+    }
+    40% {
+        -webkit-transform: scale(1.0);
+        transform: scale(1.0);
+    }
+}
+
+[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
+    display: none !important;
+}
+
+.nvd3 .nv-axis .nv-axisMaxMin text {
+    font-weight: normal; /* Here the text can be modified*/
+}
+
+[ng-hide].ng-hide-add.ng-hide-animate {
+    display: none;
+}
+
+[ng-show].ng-hide-add.ng-hide-animate {
+    display: none;
+}
+
+@media only screen and (max-width: 767px) {
+    .container{
+        padding: 0 $padding-small-horizontal;
+    }
+}
+
+// Fix for incorrect tooltip placement after fast show|hide.
+.tooltip.ng-leave {
+    transition: none !important; /* Disable transitions. */
+    animation: none 0s !important; /* Disable keyframe animations. */
+}
+
+// Fix for incorrect dropdown placement.
+.select.dropdown-menu.ng-leave {
+    transition: none !important; /* Disable transitions. */
+    animation: none 0s !important; /* Disable keyframe animations. */
+}
+.disable-animations {
+    // Use this for transitions
+    &.ng-enter,
+    &.ng-leave,
+    &.ng-animate {
+        -webkit-transition: none !important;
+        transition: none !important;
+    }
+    // Use this for keyframe animations
+    &.ng-animate {
+        -webkit-animation: none 0s;
+        animation: none 0s;
+    }
+}
+
+.center-container {
+    position: absolute !important;
+}
+
+// Fix for injecting svg icon into BS btn
+.btn--with-icon {
+    display: flex;
+    align-items: center;
+
+    span {
+        margin-left: 5px;
+    }
+}
+
+header.header-with-selector {
+    margin: 40px 0 30px;
+
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+
+
+    h1 {
+        min-height: 36px;
+        margin: 0;
+        margin-right: 8px;
+
+        font-size: 24px;
+        line-height: 36px;
+    }
+
+    div {
+        display: flex;
+        align-items: center;
+
+        &:nth-child(2) {
+            .btn-ignite {
+                margin-left: 20px;
+            }
+
+            .btn-ignite-group {
+                .btn-ignite {
+                    margin-left: 0;
+                }
+            }
+
+            .btn-ignite + .btn-ignite-group {
+                position: relative;
+                margin-left: 20px;
+
+                button {
+                    margin-left: 0;
+                }
+
+                ul.dropdown-menu {
+                    min-width: 100%;
+                }
+            }
+        }
+    }
+}
diff --git a/modules/frontend/public/stylesheets/variables.scss b/modules/frontend/public/stylesheets/variables.scss
new file mode 100644
index 0000000..bc0c2d5
--- /dev/null
+++ b/modules/frontend/public/stylesheets/variables.scss
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import "bootstrap-variables";
+
+$logo-path: "/images/logo.png";
+$input-height: 28px;
+$ignite-placeholder-color: #999999;
+$ignite-border-color: #ddd;
+$ignite-darck-border-color: #aaa;
+$ignite-border-bottom-color: $brand-primary;
+$ignite-background-color: #fff;
+// New background color for layouts without white content box
+$ignite-new-background-color: #f9f9f9;
+$ignite-header-color: #555;
+$ignite-invalid-color: $brand-primary;
+$ignite-button-border-radius: 4px;
+
+// Color palette
+$ignite-brand-primary: #ee2b27; // Red
+$ignite-brand-success: #0067b9; // Blue
+
+// Color for statuses
+$ignite-status-active: #417505;
+$ignite-status-inactive: #ee2b27;
\ No newline at end of file
diff --git a/modules/frontend/test/check-doc-links/Dockerfile b/modules/frontend/test/check-doc-links/Dockerfile
new file mode 100644
index 0000000..a9cd821
--- /dev/null
+++ b/modules/frontend/test/check-doc-links/Dockerfile
@@ -0,0 +1,31 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+FROM node:8-alpine
+
+# Install frontend & backend apps.
+RUN mkdir -p /opt/web-console
+
+# Copy source.
+WORKDIR /opt/web-console
+COPY . ./frontend
+
+# Install node modules.
+WORKDIR /opt/web-console/frontend
+RUN npm install
+
+ENTRYPOINT ["node", "test/check-doc-links/check-doc-links.js"]
diff --git a/modules/frontend/test/check-doc-links/check-doc-links.js b/modules/frontend/test/check-doc-links/check-doc-links.js
new file mode 100644
index 0000000..4ab64ed
--- /dev/null
+++ b/modules/frontend/test/check-doc-links/check-doc-links.js
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-env node*/
+
+const globby = require('globby');
+const lex = require('pug-lexer');
+const parse = require('pug-parser');
+const walk = require('pug-walk');
+const compileAttrs = require('pug-attrs');
+const {promisify} = require('util');
+const fs = require('fs');
+const readFileAsync = promisify(fs.readFile);
+const fetch = require('node-fetch');
+const ProgressBar = require('progress');
+const chalk = require('chalk');
+const tsm = require('teamcity-service-messages');
+const slash = require('slash');
+const appRoot = require('app-root-path').path;
+
+const {argv} = require('yargs')
+    .option('directory', {
+        alias: 'd',
+        describe: 'parent directory to apply glob pattern from',
+        default: appRoot
+    })
+    .option('pugs', {
+        alias: 'p',
+        describe: 'glob pattern to select templates with',
+        default: '{app,views}/**/*.pug'
+    })
+    .usage('Usage: $0 [options]')
+    .example(
+        '$0 --pugs="./**/*.pug"',
+        `look for all invalid links in all pug files in current dir and it's subdirs`
+    )
+    .help();
+
+const pugPathToAST = async(pugPath) => Object.assign(parse(lex(await readFileAsync(pugPath, 'utf8'))), {filePath: pugPath});
+
+const findLinks = (acc, ast) => {
+    walk(ast, (node) => {
+        if (node.attrs) {
+            const href = node.attrs.find((attr) => attr.name === 'href');
+
+            if (href) {
+                const compiledAttr = compileAttrs([href], {
+                    terse: false,
+                    format: 'object',
+                    runtime() {}
+                });
+
+                try {
+                    acc.push([JSON.parse(compiledAttr).href, ast.filePath]);
+                }
+                catch (e) {
+                    console.log(ast.filePath, e);
+                }
+            }
+        }
+    });
+    return acc;
+};
+
+const isDocsURL = (url) => url.startsWith('http');
+
+const isInvalidURL = (url) => fetch(url, {redirect: 'manual'}).then((res) => res.status !== 200);
+
+const first = ([value]) => value;
+
+const checkDocLinks = async(pugsGlob, onProgress = () => {}) => {
+    const pugs = await globby(pugsGlob);
+    const allAST = await Promise.all(pugs.map(pugPathToAST));
+    const allLinks = allAST.reduce(findLinks, []).filter((pair) => isDocsURL(first(pair)));
+
+    onProgress(allLinks);
+
+    const tick = (v) => {onProgress(v); return v;};
+
+    const results = await Promise.all(allLinks.map((pair) => {
+        return isInvalidURL(first(pair))
+        .then((isInvalid) => [...pair, isInvalid])
+        .then(tick)
+        .catch(tick);
+    }));
+
+    const invalidLinks = results.filter(([,, isInvalid]) => isInvalid);
+
+    return {allLinks, invalidLinks};
+};
+
+module.exports.checkDocLinks = checkDocLinks;
+
+const specReporter = (allLinks, invalidLinks) => {
+    const invalidCount = invalidLinks.length;
+
+    const format = ([link, at], i) => `\n${i + 1}. ${chalk.red(link)} ${chalk.dim('in')} ${chalk.yellow(at)}`;
+
+    console.log(`Total links: ${allLinks.length}`);
+    console.log(`Invalid links found: ${invalidCount ? chalk.red(invalidCount) : chalk.green(invalidCount)}`);
+
+    if (invalidCount) console.log(invalidLinks.map(format).join(''));
+};
+
+const teamcityReporter = (allLinks, invalidLinks) => {
+    const name = 'Checking docs links';
+    tsm.testStarted({ name });
+
+    if (invalidLinks.length > 0)
+        tsm.testFailed({ name, details: invalidLinks.map(([link, at]) => `\n ${link} in ${at}`).join('') });
+    else {
+        tsm.testStdOut(`All ${allLinks.length} are correct!`);
+        tsm.testFinished({ name: 'Checking docs links' });
+    }
+};
+
+const main = async() => {
+    let bar;
+    const updateBar = (value) => {
+        if (!bar)
+            bar = new ProgressBar('Checking links [:bar] :current/:total', {total: value.length});
+        else
+            bar.tick();
+    };
+
+    const unBackSlashedDirPath = slash(argv.directory);
+    const absolutePugGlob = `${unBackSlashedDirPath}/${argv.pugs}`;
+
+    console.log(`Looking for invalid links in ${chalk.cyan(absolutePugGlob)}.`);
+
+    const {allLinks, invalidLinks} = await checkDocLinks(absolutePugGlob, updateBar);
+
+    const reporter = process.env.TEAMCITY ? teamcityReporter : specReporter;
+    reporter(allLinks, invalidLinks);
+};
+
+main();
diff --git a/modules/frontend/test/check-doc-links/docker-compose.yml b/modules/frontend/test/check-doc-links/docker-compose.yml
new file mode 100644
index 0000000..6aca7e4
--- /dev/null
+++ b/modules/frontend/test/check-doc-links/docker-compose.yml
@@ -0,0 +1,25 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+version: '2'
+services:
+  check-doc-links:
+    build:
+      context: '../../'
+      dockerfile: './test/check-doc-links/Dockerfile'
+    environment:
+      - TEAMCITY=true
\ No newline at end of file
diff --git a/modules/frontend/test/ci/.dockerignore b/modules/frontend/test/ci/.dockerignore
new file mode 100644
index 0000000..bcac98f
--- /dev/null
+++ b/modules/frontend/test/ci/.dockerignore
@@ -0,0 +1,4 @@
+.git
+*Dockerfile*
+*docker-compose*
+*/node_modules*
\ No newline at end of file
diff --git a/modules/frontend/test/ci/Dockerfile b/modules/frontend/test/ci/Dockerfile
new file mode 100644
index 0000000..7b1d602
--- /dev/null
+++ b/modules/frontend/test/ci/Dockerfile
@@ -0,0 +1,34 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+FROM node:8-alpine
+
+ENV NPM_CONFIG_LOGLEVEL error
+
+RUN apk --no-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ add \
+ chromium xwininfo xvfb dbus eudev ttf-freefont fluxbox
+
+ENV CHROME_BIN /usr/bin/chromium-browser
+
+WORKDIR /opt/web-console/frontend
+
+COPY ./package*.json ./
+RUN npm install --no-optional
+
+COPY . .
+
+ENTRYPOINT ["npm", "test"]
diff --git a/modules/frontend/test/ci/docker-compose.yml b/modules/frontend/test/ci/docker-compose.yml
new file mode 100644
index 0000000..69667e6
--- /dev/null
+++ b/modules/frontend/test/ci/docker-compose.yml
@@ -0,0 +1,25 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+version: '2'
+services:
+  unit_tests:
+    build:
+      context: '../../'
+      dockerfile: './test/ci/Dockerfile'
+    environment:
+      - TEST_REPORTER=teamcity
\ No newline at end of file
diff --git a/modules/frontend/test/karma.conf.js b/modules/frontend/test/karma.conf.js
new file mode 100644
index 0000000..05b7e45
--- /dev/null
+++ b/modules/frontend/test/karma.conf.js
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const path = require('path');
+
+const testCfg = require('../webpack/webpack.test');
+
+module.exports = (/** @type {import('karma').Config} */ config) => {
+    config.set({
+        // Base path that will be used to resolve all patterns (eg. files, exclude).
+        basePath: path.resolve('./'),
+
+        // Frameworks to use available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+        frameworks: ['mocha'],
+
+        // List of files / patterns to load in the browser.
+        files: [
+            'node_modules/angular/angular.js',
+            'node_modules/angular-mocks/angular-mocks.js',
+            'app/**/*.spec.+(js|ts)',
+            'test/**/*.test.js'
+        ],
+
+        plugins: [
+            require('karma-chrome-launcher'),
+            require('karma-teamcity-reporter'),
+            require('karma-mocha-reporter'),
+            require('karma-webpack'),
+            require('karma-mocha')
+        ],
+
+        // Preprocess matching files before serving them to the browser
+        // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor.
+        preprocessors: {
+            '+(app|test)/**/*.+(js|ts)': ['webpack']
+        },
+
+        webpack: testCfg,
+
+        webpackMiddleware: {
+            noInfo: true
+        },
+
+        // Test results reporter to use
+        // possible values: 'dots', 'progress'
+        // available reporters: https://npmjs.org/browse/keyword/karma-reporter.
+        reporters: [process.env.TEST_REPORTER || 'mocha'],
+
+        mochaReporter: {
+            showDiff: true
+        },
+
+        // web server port
+        port: 9876,
+
+        // enable / disable colors in the output (reporters and logs)
+        colors: true,
+
+        // level of logging
+        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+        logLevel: config.LOG_INFO,
+
+        // enable / disable watching file and executing tests whenever any file changes
+        autoWatch: true,
+
+        // start these browsers
+        // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+        browsers: ['ChromeHeadlessNoSandbox'],
+        customLaunchers: {
+            ChromeHeadlessNoSandbox: {
+                base: 'ChromeHeadless',
+                flags: ['--no-sandbox']
+            },
+            ChromeDebug: {
+                base: 'Chrome',
+                flags: [
+                    '--start-maximized',
+                    '--auto-open-devtools-for-tabs'
+                ],
+                debug: true
+            }
+        },
+
+        // Continuous Integration mode
+        // if true, Karma captures browsers, runs the tests and exits
+        singleRun: true,
+
+        // Concurrency level
+        // how many browser should be started simultaneous
+        concurrency: Infinity,
+
+        client: {
+            mocha: {
+                ui: 'tdd'
+            }
+        }
+    });
+};
diff --git a/modules/frontend/test/unit/SqlTypes.test.js b/modules/frontend/test/unit/SqlTypes.test.js
new file mode 100644
index 0000000..6709293
--- /dev/null
+++ b/modules/frontend/test/unit/SqlTypes.test.js
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import SqlTypes from '../../app/services/SqlTypes.service';
+import {suite, test} from 'mocha';
+import {assert} from 'chai';
+
+const INSTANCE = new SqlTypes();
+
+suite('SqlTypesTestsSuite', () => {
+    test('validIdentifier', () => {
+        assert.equal(INSTANCE.validIdentifier('myIdent'), true);
+        assert.equal(INSTANCE.validIdentifier('java.math.BigDecimal'), false);
+        assert.equal(INSTANCE.validIdentifier('2Demo'), false);
+        assert.equal(INSTANCE.validIdentifier('abra kadabra'), false);
+        assert.equal(INSTANCE.validIdentifier(), false);
+        assert.equal(INSTANCE.validIdentifier(null), false);
+        assert.equal(INSTANCE.validIdentifier(''), false);
+        assert.equal(INSTANCE.validIdentifier(' '), false);
+    });
+
+    test('isKeyword', () => {
+        assert.equal(INSTANCE.isKeyword('group'), true);
+        assert.equal(INSTANCE.isKeyword('Group'), true);
+        assert.equal(INSTANCE.isKeyword('select'), true);
+        assert.equal(INSTANCE.isKeyword('abra kadabra'), false);
+        assert.equal(INSTANCE.isKeyword(), false);
+        assert.equal(INSTANCE.isKeyword(null), false);
+        assert.equal(INSTANCE.isKeyword(''), false);
+        assert.equal(INSTANCE.isKeyword(' '), false);
+    });
+
+    test('findJdbcType', () => {
+        assert.equal(INSTANCE.findJdbcType(0).dbName, 'NULL');
+        assert.equal(INSTANCE.findJdbcType(5555).dbName, 'Unknown');
+    });
+});
diff --git a/modules/frontend/test/unit/UserAuth.test.js b/modules/frontend/test/unit/UserAuth.test.js
new file mode 100644
index 0000000..c8d63a2
--- /dev/null
+++ b/modules/frontend/test/unit/UserAuth.test.js
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// import AuthService from '../../app/modules/user/Auth.service';
+
+import {suite, test} from 'mocha';
+
+suite('AuthServiceTestsSuite', () => {
+    test('SignIn', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+
+    test('SignUp', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+
+    test('Logout', (done) => {
+        // TODO IGNITE-3262 Add test.
+        done();
+    });
+});
diff --git a/modules/frontend/test/unit/defaultName.filter.test.js b/modules/frontend/test/unit/defaultName.filter.test.js
new file mode 100644
index 0000000..2e66c9b
--- /dev/null
+++ b/modules/frontend/test/unit/defaultName.filter.test.js
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import defaultName from '../../app/filters/default-name.filter';
+
+import {assert} from 'chai';
+
+const instance = defaultName();
+
+suite('defaultName', () => {
+    test('defaultName filter', () => {
+        let undef;
+
+        assert.equal(instance(''), '<default>');
+        assert.equal(instance(null), '<default>');
+        assert.equal(instance(), '<default>');
+        assert.equal(instance('', false), '<default>');
+        assert.equal(instance(null, false), '<default>');
+        assert.equal(instance(undef, false), '<default>');
+        assert.equal(instance('', true), '&lt;default&gt;');
+        assert.equal(instance(null, true), '&lt;default&gt;');
+        assert.equal(instance(undef, true), '&lt;default&gt;');
+        assert.equal(instance('name', false), 'name');
+        assert.equal(instance('name', true), 'name');
+    });
+});
diff --git a/modules/frontend/tsconfig.json b/modules/frontend/tsconfig.json
new file mode 100644
index 0000000..531a381
--- /dev/null
+++ b/modules/frontend/tsconfig.json
@@ -0,0 +1,15 @@
+{
+    "compilerOptions": {
+        "allowSyntheticDefaultImports": true,
+        "target": "ES2017",
+        "moduleResolution": "Node",
+        "module": "esNext",
+        "noEmit": true,
+        "allowJs": true,
+        "checkJs": true,
+        "baseUrl": "."
+    },
+    "exclude": [
+        "build"
+    ]
+}
\ No newline at end of file
diff --git a/modules/frontend/views/base.pug b/modules/frontend/views/base.pug
new file mode 100644
index 0000000..b14cdac
--- /dev/null
+++ b/modules/frontend/views/base.pug
@@ -0,0 +1,23 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+permanent-notifications
+web-console-header
+    web-console-header-content
+
+web-console-sidebar
+
+ui-view.content
\ No newline at end of file
diff --git a/modules/frontend/views/index.pug b/modules/frontend/views/index.pug
new file mode 100644
index 0000000..71d5363
--- /dev/null
+++ b/modules/frontend/views/index.pug
@@ -0,0 +1,45 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+doctype html
+html
+    head
+        base(href='/')
+
+        meta(http-equiv='content-type' content='text/html; charset=utf-8')
+        meta(http-equiv='content-language' content='en')
+        meta(http-equiv="Cache-control" content="no-cache, no-store, must-revalidate")
+        meta(http-equiv="Pragma" content="no-cache")
+        meta(http-equiv='X-UA-Compatible' content='IE=Edge')
+        meta(name='viewport' content='width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0')
+
+        title(ng-bind='tfMetaTags.title')
+        meta(ng-repeat='(key, value) in tfMetaTags.properties' name='{{::key}}' content='{{::value}}')
+
+        meta(name='fragment' content='!')
+
+    body.theme-line.body-overlap(ng-class='{ "demo-mode": IgniteDemoMode }')
+
+        .splash.splash-max-foreground(hide-on-state-change)
+            .splash-wrapper
+                .spinner
+                    .bounce1
+                    .bounce2
+                    .bounce3
+
+                .splash-wellcome Loading...
+
+        .wrapper(ui-view='')
diff --git a/modules/frontend/views/public.pug b/modules/frontend/views/public.pug
new file mode 100644
index 0000000..934ac56
--- /dev/null
+++ b/modules/frontend/views/public.pug
@@ -0,0 +1,20 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+web-console-header(hide-menu-button='true')
+    .web-console-header-content__title Management console for Apache Ignite
+.content(ui-view='page')
+web-console-footer.web-console-footer__page-bottom
\ No newline at end of file
diff --git a/modules/frontend/views/sql/cache-metadata.tpl.pug b/modules/frontend/views/sql/cache-metadata.tpl.pug
new file mode 100644
index 0000000..ccffeae
--- /dev/null
+++ b/modules/frontend/views/sql/cache-metadata.tpl.pug
@@ -0,0 +1,41 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+.popover.cache-metadata(tabindex='-1' ignite-loading='loadingCacheMetadata' ignite-loading-text='Loading metadata...' ng-init='importMetadata()')
+    h3.popover-title
+        label.labelField Metadata for caches:
+        button.close(id='cache-metadata-close' ng-click='$hide()') &times;
+        .input-tip
+            input.form-control(type='text' ng-model='metaFilter' placeholder='Filter metadata...')
+    .popover-content(ng-if='metadata && metadata.length > 0')
+        treecontrol.tree-classic(tree-model='metadata' options='metaOptions' filter-expression='metaFilter')
+            span(ng-switch='node.type')
+                span(ng-switch-when='type' ng-dblclick='dblclickMetadata(paragraph, node)')
+                    i.fa.fa-table
+                    label.clickable
+                        div.node-display(ng-bind='node.displayName' ng-attr-title='{{node.displayName}}')
+                span(ng-switch-when='plain')
+                    label {{node.name}}
+                span(ng-switch-when='field' ng-dblclick='dblclickMetadata(paragraph, node)')
+                    i.fa(ng-class='node.system ? "fa-file-text-o" : "fa-file-o"')
+                    label.clickable {{node.name}} [{{node.clazz}}]
+                label(ng-switch-when='indexes') {{node.name}}
+                label(ng-switch-when='index') {{node.name}}
+                span(ng-switch-when='index-field' ng-dblclick='dblclickMetadata(paragraph, node)')
+                    i.fa(ng-class='node.order ? "fa-sort-amount-desc" : "fa-sort-amount-asc"')
+                    label.clickable {{node.name}}
+    .popover-content(ng-if='!metadata || metadata.length == 0')
+        label.content-empty No types found
+    h3.popover-footer Double click to paste into editor
diff --git a/modules/frontend/views/sql/chart-settings.tpl.pug b/modules/frontend/views/sql/chart-settings.tpl.pug
new file mode 100644
index 0000000..054067f
--- /dev/null
+++ b/modules/frontend/views/sql/chart-settings.tpl.pug
@@ -0,0 +1,40 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.popover.settings(tabindex='-1' style='width: 300px')
+    .arrow
+    h3.popover-title(style='color: black') Chart settings
+    button.close(id='chart-settings-close' ng-click='$hide()') &times;
+    .popover-content
+        form.form-horizontal.chart-settings(name='chartSettingsForm' novalidate)
+            .form-group.chart-settings
+                -var btnClass = 'col.value < 0 ? "btn-success" : "btn-default"'
+
+                label All columns (drag columns to axis)
+                ul.chart-settings-columns-list(dnd-list='paragraph.chartColumns' dnd-allowed-types='[]')
+                    li(ng-repeat='col in paragraph.chartColumns track by $index')
+                        .btn.btn-default.btn-chart-column-movable(ng-class=btnClass dnd-draggable='col' dnd-effect-allowed='copy') {{col.label}}
+                label X axis (accept only one column)
+                ul.chart-settings-columns-list(dnd-list='paragraph.chartKeyCols' dnd-drop='chartAcceptKeyColumn(paragraph, item)')
+                    li(ng-repeat='col in paragraph.chartKeyCols track by $index')
+                        .btn.btn-default.btn-chart-column(ng-class=btnClass) {{col.label}}
+                            i.fa.fa-close(ng-click='chartRemoveKeyColumn(paragraph, $index)')
+                label Y axis (accept only numeric columns)
+                ul.chart-settings-columns-list(dnd-list='paragraph.chartValCols' dnd-drop='chartAcceptValColumn(paragraph, item)')
+                    li(ng-repeat='col in paragraph.chartValCols track by $index')
+                        .btn.btn-default.btn-chart-column(ng-style='chartColor($index)') {{col.label}}
+                            button.btn-chart-column-agg-fx.select-toggle(ng-change='applyChartSettings(paragraph)' ng-show='paragraphTimeSpanVisible(paragraph)' ng-style='chartColor($index)' ng-model='col.aggFx' placeholder='...' bs-select bs-options='item for item in aggregateFxs' tabindex='-1')
+                            i.fa.fa-close(ng-click='chartRemoveValColumn(paragraph, $index)')
diff --git a/modules/frontend/views/sql/paragraph-rate.tpl.pug b/modules/frontend/views/sql/paragraph-rate.tpl.pug
new file mode 100644
index 0000000..bd65208
--- /dev/null
+++ b/modules/frontend/views/sql/paragraph-rate.tpl.pug
@@ -0,0 +1,46 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.popover.settings.refresh-rate(tabindex='-1')
+    h3.popover-title Refresh rate
+    button.close(ng-click='$hide()')
+        svg(ignite-icon='cross')
+    .popover-content
+        form.theme--ignite(name='popoverForm' novalidate ng-init='refreshRate = {}')
+            .form-field.form-field__text.ignite-form-field
+                .form-field__control
+                    input(ng-init='refreshRate.value = paragraph.rate.value' ng-model='refreshRate.value' type='number' min='1' required ignite-auto-focus)
+                .form-field__control
+                    button.select-toggle(ng-init='refreshRate.unit = paragraph.rate.unit'  ng-model='refreshRate.unit' required placeholder='Time unit' bs-select bs-options='item.value as item.label for item in timeUnit' tabindex='0')
+            .form-field.form-field__text.ignite-form-field
+                .actions
+                    button.btn-ignite.btn-ignite--primary(
+                        ng-disabled='popoverForm.$invalid'
+                        ng-click='startRefresh(paragraph, refreshRate.value, refreshRate.unit); $hide()'
+                        ng-hide='paragraph.rate.installed'
+                    ) Start
+
+                    button.btn-ignite.btn-ignite--primary(
+                        ng-disabled='popoverForm.$invalid || (refreshRate.unit === paragraph.rate.unit && refreshRate.value === paragraph.rate.value)'
+                        ng-click='startRefresh(paragraph, refreshRate.value, refreshRate.unit); $hide()'
+                        ng-hide='!paragraph.rate.installed'
+                    ) Start new
+
+                    button.btn-ignite.btn-ignite--primary(
+                        ng-click='stopRefresh(paragraph); $hide()'
+                        ng-hide='!paragraph.rate.installed'
+                    ) Stop
+
diff --git a/modules/frontend/views/templates/agent-download.tpl.pug b/modules/frontend/views/templates/agent-download.tpl.pug
new file mode 100644
index 0000000..46acfed
--- /dev/null
+++ b/modules/frontend/views/templates/agent-download.tpl.pug
@@ -0,0 +1,69 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+.modal.modal--ignite.theme--ignite.center(tabindex='-1' role='dialog')
+    .modal-dialog(ng-switch='ctrl.status')
+        .modal-content(ng-switch-when='agentMissing')
+            .modal-header.header
+                h4.modal-title
+                    span Connection to Ignite Web Agent is not established
+            .modal-body.agent-download
+                p Please download and run #[a(href='/api/v1/downloads/agent' target='_self') ignite-web-agent] to use this functionality:
+                    ul
+                        li Download and unzip #[a(href='/api/v1/downloads/agent' target='_self') ignite-web-agent] archive
+                        li Run shell file #[b ignite-web-agent.{sh|bat}]
+                p Refer to #[b README.txt] in the ignite-web-agent folder for more information.
+                .modal-advanced-options
+                    i.fa(ng-class='showToken ? "fa-chevron-circle-down" : "fa-chevron-circle-right"' ng-click='showToken = !showToken')
+                    a(ng-click='showToken = !showToken') {{showToken ? 'Hide security token...' : 'Show security token...'}}
+                div(ng-show='showToken')
+                    +form-field__text({
+                        label: 'Security Token:',
+                        model: 'ctrl.securityToken',
+                        tip: 'The security token is used for authentication of web agent',
+                        name: '"securityToken"'
+                    })(
+                        autocomplete='security-token'
+                        ng-disabled='::true'
+                        copy-input-value-button='Copy security token to clipboard'
+                    )
+
+            .modal-footer
+                div
+                    button.btn-ignite.btn-ignite--link-success(ng-click='ctrl.back()') {{::ctrl.backText}}
+                    a.btn-ignite.btn-ignite--success(href='/api/v1/downloads/agent' target='_self') Download agent
+
+        .modal-content(ng-switch-when='nodeMissing')
+            .modal-header.header
+                h4.modal-title
+                    i.fa.fa-download
+                    span Connection to cluster was lost or can't be established
+
+            .modal-body.agent-download
+                p Connection to Ignite Web Agent is established, but agent failed to connect to cluster
+                p Please check the following:
+                p
+                    ul
+                        li Ignite Grid is up and Ignite REST server started (copy "ignite-rest-http" folder from libs/optional/ to libs/)
+                        li In agent settings check URI for connect to Ignite REST server
+                        li Check agent logs for errors
+                        li Refer to #[b README.txt] in the ignite-web-agent folder for more information.
+
+            .modal-footer
+                div
+                    button.btn-ignite.btn-ignite--link-success(ng-click='ctrl.back()') {{::ctrl.backText}}
diff --git a/modules/frontend/views/templates/alert.tpl.pug b/modules/frontend/views/templates/alert.tpl.pug
new file mode 100644
index 0000000..d30d2fd
--- /dev/null
+++ b/modules/frontend/views/templates/alert.tpl.pug
@@ -0,0 +1,21 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.alert(ng-show='type' ng-class='[type ? "alert-" + type : null]')
+    button.close(type='button', ng-if='dismissable', ng-click='$hide()') &times;
+    i.alert-icon(ng-if='icon' ng-class='[icon]')
+    span.alert-title(ng-bind-html='title')
+    span.alert-content(ng-bind-html='content')
diff --git a/modules/frontend/views/templates/batch-confirm.tpl.pug b/modules/frontend/views/templates/batch-confirm.tpl.pug
new file mode 100644
index 0000000..874979b
--- /dev/null
+++ b/modules/frontend/views/templates/batch-confirm.tpl.pug
@@ -0,0 +1,47 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.modal.modal--ignite.theme--ignite(tabindex='-1' role='dialog')
+    .modal-dialog
+        .modal-content
+            .modal-header
+                h4.modal-title
+                    svg(ignite-icon='attention')
+                    | Confirmation
+                button.close(type='button' aria-label='Close' ng-click='cancel()')
+                     svg(ignite-icon="cross")
+            .modal-body(ng-show='content')
+                p(ng-bind-html='content')
+            .modal-footer
+                .checkbox.labelField(style='margin-top: 7px')
+                    label
+                        input(type='checkbox' ng-model='applyToAll')
+                        | Apply to all
+                button.btn-ignite.btn-ignite--secondary(
+                    id='batch-confirm-btn-cancel'
+                    ng-click='cancel()'
+                    type='button'
+                ) Cancel
+                button.btn-ignite.btn-ignite--secondary(
+                    id='batch-confirm-btn-skip'
+                    ng-click='skip(applyToAll)'
+                    type='button'
+                ) Skip
+                button.btn-ignite.btn-ignite--success(
+                    id='batch-confirm-btn-overwrite'
+                    ng-click='overwrite(applyToAll)'
+                    type='button'
+                ) Overwrite
diff --git a/modules/frontend/views/templates/confirm.tpl.pug b/modules/frontend/views/templates/confirm.tpl.pug
new file mode 100644
index 0000000..581aa37
--- /dev/null
+++ b/modules/frontend/views/templates/confirm.tpl.pug
@@ -0,0 +1,34 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.modal.modal--ignite.theme--ignite(tabindex='-1' role='dialog')
+    .modal-dialog.modal-dialog--adjust-height
+        .modal-content
+            .modal-header
+                h4.modal-title
+                    span Confirmation
+                button.close(type='button' aria-label='Close' ng-click='confirmCancel()')
+                     svg(ignite-icon="cross")
+            .modal-body(ng-show='content')
+                p(ng-bind-html='content')
+            .modal-footer
+                div
+                    button#confirm-btn-cancel.btn-ignite.btn-ignite--link-success(ng-click='confirmCancel()') Cancel
+
+                    button#confirm-btn-no.btn-ignite.btn-ignite--link-success(ng-if='yesNo' ng-click='confirmNo()') No
+                    button#confirm-btn-yes.btn-ignite.btn-ignite--success(ignite-auto-focus ng-if='yesNo' ng-click='confirmYes()') Yes
+
+                    button#confirm-btn-ok.btn-ignite.btn-ignite--success(ignite-auto-focus ng-if='!yesNo' ng-click='confirmYes()') Confirm
diff --git a/modules/frontend/views/templates/demo-info.tpl.pug b/modules/frontend/views/templates/demo-info.tpl.pug
new file mode 100644
index 0000000..437c8a0
--- /dev/null
+++ b/modules/frontend/views/templates/demo-info.tpl.pug
@@ -0,0 +1,53 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.modal.modal--ignite.theme--ignite.center(role='dialog')
+    .modal-dialog
+        .modal-content
+            #errors-container.modal-header.header
+                button.close(ng-click='close()' aria-hidden='true')
+                    svg(ignite-icon="cross")
+                h4.modal-title
+                    svg(ignite-icon="attention")
+                    | {{title}}
+            .modal-body
+                div(ng-bind-html='message')
+
+                div(ng-hide='hasAgents')
+                    h4
+                        i.fa.fa-download.fa-cursor-default
+                        | &nbsp;How To Start Demo
+                    ul
+                        li
+                            a(ng-href='{{downloadAgentHref}}' target='_self') #[b Download]
+                            | &nbsp; and unzip ignite-web-agent archive
+                        li #[b Run] shell file ignite-web-agent.{sh|bat}
+
+                div(ng-hide='!hasAgents')
+                    h4
+                        i.fa.fa-star-o.fa-cursor-default
+                        | &nbsp;Start Demo
+                    ul
+                        li Web Agent is already started
+                        li Close dialog and try Web Console
+
+            .modal-footer
+                .ng-animate-disabled(ng-if='!hasAgents')
+                    button.btn-ignite.btn-ignite--link-success(ng-click='close()') Close
+                    a.btn-ignite.btn-ignite--success(ng-href='{{downloadAgentHref}}' target='_self') Download agent
+
+                .ng-animate-disabled(ng-if='hasAgents')
+                    button.btn-ignite.btn-ignite--success(ng-click='close()') Close
diff --git a/modules/frontend/views/templates/dropdown.tpl.pug b/modules/frontend/views/templates/dropdown.tpl.pug
new file mode 100644
index 0000000..1ddd775
--- /dev/null
+++ b/modules/frontend/views/templates/dropdown.tpl.pug
@@ -0,0 +1,24 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+ul.dropdown-menu(tabindex='-1' role='menu' ng-show='content && content.length')
+    li(role='presentation' ui-sref-active='active' ng-class='{divider: item.divider, active: item.active}' ng-repeat='item in content')
+        a(role='menuitem' tabindex='-1' ui-sref='{{item.sref}}' ng-if='!item.action && !item.divider && item.sref' ng-bind='item.text')
+        a(role='menuitem' tabindex='-1' ng-href='{{item.href}}' ng-if='!item.action && !item.divider && item.href' target='{{item.target || ""}}' ng-bind='item.text')
+        a(role='menuitem' tabindex='-1' ng-if='!item.action && !item.divider && item.click' ng-click='$eval(item.click);$hide()' ng-bind='item.text')
+        div(role='menuitem' ng-if='item.action')
+            i.fa.pull-right(class='{{ item.action.icon }}' ng-click='item.action.click(item.data)' bs-tooltip data-title='{{ item.action.tooltip }}' data-trigger='hover' data-placement='bottom')
+            div: a(ui-sref='{{ item.sref }}' ng-bind='item.text')
diff --git a/modules/frontend/views/templates/getting-started.tpl.pug b/modules/frontend/views/templates/getting-started.tpl.pug
new file mode 100644
index 0000000..8b0a03c
--- /dev/null
+++ b/modules/frontend/views/templates/getting-started.tpl.pug
@@ -0,0 +1,39 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+.modal.modal--ignite.theme--ignite.center(role='dialog')
+    .modal-dialog
+        .modal-content
+            #errors-container.modal-header.header
+                button.close(ng-click='close()' aria-hidden='true')
+                    svg(ignite-icon="cross")
+                h4.modal-title
+                    | {{title}}
+            .modal-body
+                .getting-started.row(ng-bind-html='message')
+            .modal-footer
+                div
+                    +form-field__checkbox({
+                        label: 'Do not show this window again',
+                        model: 'ui.dontShowGettingStarted',
+                        name: '"dontShowGettingStarted"'
+                    })
+                div
+                    a.btn-ignite.btn-ignite--link-success(ng-disabled='isFirst()' ng-click='!isFirst() && prev()') Prev
+                    a.btn-ignite.btn-ignite--link-success(ng-disabled='isLast()' ng-click='!isLast() && next()') Next
+                    a.btn-ignite.btn-ignite--success(ng-click='close()') Close
diff --git a/modules/frontend/views/templates/message.tpl.pug b/modules/frontend/views/templates/message.tpl.pug
new file mode 100644
index 0000000..bfe4c18
--- /dev/null
+++ b/modules/frontend/views/templates/message.tpl.pug
@@ -0,0 +1,32 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.modal.modal--ignite.theme--ignite.center(tabindex='-1' role='dialog')
+    .modal-dialog
+        .modal-content
+            .modal-header
+                button.close(ng-click='$hide()' aria-hidden='true')
+                    svg(ignite-icon="cross")
+                h4.modal-title
+                    svg(ignite-icon="attention")
+                    | {{title}}
+            .modal-body(ng-show='content')
+                .modal-body--inner-content
+                    p(ng-bind-html='content.join("<br/>")')
+            .modal-footer
+                div(ng-show='meta') {{meta}}
+                div
+                    button.btn-ignite.btn-ignite--success(ng-click='$hide()') Ok
diff --git a/modules/frontend/views/templates/validation-error.tpl.pug b/modules/frontend/views/templates/validation-error.tpl.pug
new file mode 100644
index 0000000..2e0e423
--- /dev/null
+++ b/modules/frontend/views/templates/validation-error.tpl.pug
@@ -0,0 +1,25 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.popover.validation-error
+    .arrow
+    .popover-content
+        table
+            tr
+                td
+                    label#popover-validation-message {{content}}
+                td
+                    button.close(id='popover-btn-close' ng-click='$hide()') &times;
diff --git a/modules/frontend/webpack/webpack.common.js b/modules/frontend/webpack/webpack.common.js
new file mode 100644
index 0000000..327fe7a
--- /dev/null
+++ b/modules/frontend/webpack/webpack.common.js
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const path = require('path');
+const webpack = require('webpack');
+
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const ProgressBarPlugin = require('progress-bar-webpack-plugin');
+
+const eslintFormatter = require('eslint-formatter-friendly');
+
+const basedir = path.join(__dirname, '../');
+const contentBase = path.join(basedir, 'public');
+const app = path.join(basedir, 'app');
+
+/** @type {webpack.Configuration} */
+const config = {
+    node: {
+        fs: 'empty'
+    },
+    // Entry points.
+    entry: {
+        app: path.join(basedir, 'index.js'),
+        browserUpdate: path.join(app, 'browserUpdate', 'index.js')
+    },
+
+    // Output system.
+    output: {
+        path: path.resolve('build'),
+        filename: '[name].[chunkhash].js',
+        publicPath: '/'
+    },
+
+    // Resolves modules.
+    resolve: {
+        // A list of module source folders.
+        alias: {
+            app,
+            images: path.join(basedir, 'public/images'),
+            views: path.join(basedir, 'views')
+        },
+        extensions: ['.wasm', '.mjs', '.js', '.ts', '.json']
+    },
+
+    module: {
+        rules: [
+            // Exclude tpl.pug files to import in bundle.
+            {
+                test: /^(?:(?!tpl\.pug$).)*\.pug$/, // TODO: check this regexp for correct.
+                use: {
+                    loader: 'pug-html-loader',
+                    options: {
+                        basedir
+                    }
+                }
+            },
+
+            // Render .tpl.pug files to assets folder.
+            {
+                test: /\.tpl\.pug$/,
+                use: [
+                    'file-loader?exports=false&name=assets/templates/[name].[hash].html',
+                    `pug-html-loader?exports=false&basedir=${basedir}`
+                ]
+            },
+            { test: /\.worker\.js$/, use: { loader: 'worker-loader' } },
+            {
+                test: /\.(js|ts)$/,
+                enforce: 'pre',
+                exclude: [/node_modules/],
+                use: [{
+                    loader: 'eslint-loader',
+                    options: {
+                        formatter: eslintFormatter,
+                        context: process.cwd()
+                    }
+                }]
+            },
+            {
+                test: /\.(js|ts)$/,
+                exclude: /node_modules/,
+                use: 'babel-loader'
+            },
+            {
+                test: /\.(ttf|eot|svg|woff(2)?)(\?v=[\d.]+)?(\?[a-z0-9#-]+)?$/,
+                exclude: [contentBase, /\.icon\.svg$/],
+                use: 'file-loader?name=assets/fonts/[name].[ext]'
+            },
+            {
+                test: /\.icon\.svg$/,
+                use: {
+                    loader: 'svg-sprite-loader',
+                    options: {
+                        symbolRegExp: /\w+(?=\.icon\.\w+$)/,
+                        symbolId: '[0]'
+                    }
+                }
+            },
+            {
+                test: /.*\.url\.svg$/,
+                include: [contentBase],
+                use: 'file-loader?name=assets/fonts/[name].[ext]'
+            },
+            {
+                test: /\.(jpe?g|png|gif)$/i,
+                use: 'file-loader?name=assets/images/[name].[hash].[ext]'
+            },
+            {
+                test: require.resolve('jquery'),
+                use: [
+                    'expose-loader?$',
+                    'expose-loader?jQuery'
+                ]
+            },
+            {
+                test: require.resolve('nvd3'),
+                use: 'expose-loader?nv'
+            }
+        ]
+    },
+
+    optimization: {
+        splitChunks: {
+            chunks: 'all'
+        }
+    },
+
+    // Load plugins.
+    plugins: [
+        new webpack.ProvidePlugin({
+            $: 'jquery',
+            'window.jQuery': 'jquery',
+            _: 'lodash',
+            nv: 'nvd3',
+            io: 'socket.io-client'
+        }),
+        new webpack.optimize.AggressiveMergingPlugin({moveToParents: true}),
+        new HtmlWebpackPlugin({
+            template: path.join(basedir, './views/index.pug')
+        }),
+        new CopyWebpackPlugin([
+            { context: 'public', from: '**/*.{png,svg,ico}' }
+        ]),
+        new ProgressBarPlugin()
+    ]
+};
+
+module.exports = config;
diff --git a/modules/frontend/webpack/webpack.dev.js b/modules/frontend/webpack/webpack.dev.js
new file mode 100644
index 0000000..59e3d45
--- /dev/null
+++ b/modules/frontend/webpack/webpack.dev.js
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const merge = require('webpack-merge');
+
+const path = require('path');
+
+const commonCfg = require('./webpack.common');
+
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+
+const backendUrl = process.env.BACKEND_URL || 'http://localhost:3000';
+const webpackDevServerHost = process.env.HOST || '0.0.0.0';
+const webpackDevServerPort = process.env.PORT || 9000;
+
+console.log(`Backend url: ${backendUrl}`);
+
+module.exports = merge(commonCfg, {
+    mode: 'development',
+    devtool: 'source-map',
+    watch: true,
+    module: {
+        exprContextCritical: false,
+        rules: [
+            {
+                test: /\.css$/,
+                use: ['style-loader', 'css-loader']
+            },
+            {
+                test: /\.scss$/,
+                use: [
+                    MiniCssExtractPlugin.loader, // style-loader does not work with styles in IgniteModules
+                    {
+                        loader: 'css-loader',
+                        options: {
+                            sourceMap: true
+                        }
+                    },
+                    {
+                        loader: 'sass-loader',
+                        options: {
+                            sourceMap: true,
+                            includePaths: [ path.join(__dirname, '../') ]
+                        }
+                    }
+                ]
+            }
+        ]
+    },
+    plugins: [
+        new MiniCssExtractPlugin({filename: 'assets/css/[name].css'})
+    ],
+    devServer: {
+        compress: true,
+        historyApiFallback: true,
+        disableHostCheck: true,
+        contentBase: path.resolve('build'),
+        inline: true,
+        proxy: {
+            '/socket.io': {
+                target: backendUrl,
+                ws: true,
+                secure: false
+            },
+            '/agents': {
+                target: backendUrl,
+                ws: true,
+                secure: false
+            },
+            '/api/*': {
+                target: backendUrl,
+                secure: false
+            }
+        },
+        watchOptions: {
+            aggregateTimeout: 1000,
+            poll: 2000
+        },
+        stats: 'errors-only',
+        host: webpackDevServerHost,
+        port: webpackDevServerPort
+    }
+});
diff --git a/modules/frontend/webpack/webpack.prod.js b/modules/frontend/webpack/webpack.prod.js
new file mode 100644
index 0000000..fa6374e
--- /dev/null
+++ b/modules/frontend/webpack/webpack.prod.js
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const path = require('path');
+const merge = require('webpack-merge');
+
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
+
+const commonCfg = require('./webpack.common');
+
+const basedir = path.join(__dirname, '../');
+
+module.exports = merge(commonCfg, {
+    bail: true, // Cancel build on error.
+    mode: 'production',
+    module: {
+        rules: [
+            {
+                test: /\.css$/,
+                use: [MiniCssExtractPlugin.loader, 'css-loader']
+            },
+            {
+                test: /\.scss$/,
+                use: [MiniCssExtractPlugin.loader, 'css-loader', {
+                    loader: 'sass-loader',
+                    options: {
+                        includePaths: [basedir]
+                    }
+                }]
+            }
+        ]
+    },
+    plugins: [
+        new MiniCssExtractPlugin({filename: 'assets/css/[name].[hash].css'})
+    ],
+    optimization: {
+        minimizer: [
+            new UglifyJSPlugin({
+                uglifyOptions: {
+                    keep_fnames: true,
+                    keep_classnames: true
+                }
+            })
+        ]
+    }
+});
diff --git a/modules/frontend/webpack/webpack.test.js b/modules/frontend/webpack/webpack.test.js
new file mode 100644
index 0000000..c6d90df
--- /dev/null
+++ b/modules/frontend/webpack/webpack.test.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const merge = require('webpack-merge');
+const commonCfg = require('./webpack.common');
+
+module.exports = merge(commonCfg, {
+    mode: 'development',
+    cache: true,
+    node: {
+        fs: 'empty',
+        child_process: 'empty'
+    },
+
+    // Entry points.
+    entry: null,
+
+    // Output system.
+    output: null,
+    optimization: {
+        splitChunks: {
+            chunks: 'async'
+        }
+    },
+    module: {
+        exprContextCritical: false,
+        rules: [
+            {test: /\.s?css$/, use: ['ignore-loader']}
+        ]
+    }
+});
diff --git a/modules/web-agent/.gitignore b/modules/web-agent/.gitignore
new file mode 100644
index 0000000..57dd45e
--- /dev/null
+++ b/modules/web-agent/.gitignore
@@ -0,0 +1,2 @@
+logs/*.log.*
+jdbc-drivers/*.jar
diff --git a/modules/web-agent/README.txt b/modules/web-agent/README.txt
new file mode 100644
index 0000000..6b29902
--- /dev/null
+++ b/modules/web-agent/README.txt
@@ -0,0 +1,123 @@
+Ignite Web Agent
+======================================
+Ignite Web Agent is a java standalone application that allow to connect Ignite Grid to Ignite Web Console.
+Ignite Web Agent communicates with grid nodes via REST interface and connects to Ignite Web Console via web-socket.
+
+Two main functions of Ignite Web Agent:
+ 1. Proxy between Ignite Web Console and Ignite Grid to execute SQL statements and collect metrics for monitoring.
+   You may need to specify URI for connect to Ignite REST server via "-n" option.
+
+ 2. Proxy between Ignite Web Console and user RDBMS to collect database metadata for later indexed types configuration.
+   You may need to copy JDBC driver into "./jdbc-drivers" subfolder or specify path via "-d" option.
+
+Usage example:
+  ignite-web-agent.sh
+
+Configuration file:
+  Should be a file with simple line-oriented format as described here: http://docs.oracle.com/javase/7/docs/api/java/util/Properties.html#load(java.io.Reader)
+
+  Available entries names:
+    tokens
+    server-uri
+    node-uri
+    node-login
+    node-password
+    driver-folder
+    node-key-store
+    node-key-store-password
+    node-trust-store
+    node-trust-store-password
+    server-key-store
+    server-key-store-password
+    server-trust-store
+    server-trust-store-password
+    cipher-suites
+
+  Example configuration file:
+    tokens=1a2b3c4d5f,2j1s134d12
+    server-uri=https://console.example.com
+    node-uri=http://10.0.0.1:8080,http://10.0.0.2:8080
+
+Security tokens:
+  1) By default security token of current user will be included into "default.properties" inside downloaded "ignite-web-agent-x.x.x.zip".
+  2) One can get/reset token in Web Console profile (https://<your_console_address>/settings/profile).
+  3) One may specify several comma-separated list of tokens using configuration file or command line arguments of web agent.
+
+Ignite Web agent requirements:
+  1) In order to communicate with web agent Ignite node should be started with REST server (copy "ignite-rest-http" folder from "libs/optional/" to "libs/").
+  2) Configure web agent server-uri property with address where Web Console is running.
+  3) Configure web agent node-uri property with Ignite nodes URI(s).
+
+Options:
+  -h, --help
+    Print this help message
+  -c, --config
+    Path to agent property file
+    Default value: default.properties
+  -d, --driver-folder
+    Path to folder with JDBC drivers
+    Default value: ./jdbc-drivers
+  -n, --node-uri
+    Comma-separated list of URIs for connect to Ignite node via REST
+    Default value: http://localhost:8080
+  -nl, --node-login
+    User name that will be used to connect to secured cluster
+  -np, --node-password
+    Password that will be used to connect to secured cluster
+  -s, --server-uri
+    URI for connect to Ignite Console via web-socket protocol
+    Default value: http://localhost:3000
+  -t, --tokens
+     User's tokens separated by comma used to connect to Ignite Console.
+  -nks, --node-key-store
+    Path to key store that will be used to connect to cluster
+  -nksp, --node-key-store-password
+    Optional password for node key store
+  -nts, --node-trust-store
+    Path to trust store that will be used to connect to cluster
+  -ntsp, --node-trust-store-password
+    Optional password for node trust store
+  -sks, --server-key-store
+    Path to key store that will be used to connect to Web server
+  -sksp, --server-key-store-password
+    Optional password for server key store
+  -sts, --server-trust-store
+    Path to trust store that will be used to connect to Web server
+  -stsp, --server-trust-store-password
+    Optional password for server trust store
+  -cs, --cipher-suites
+     Optional comma-separated list of SSL cipher suites to be used to connect
+     to server and cluster
+
+How to build:
+  To build from sources run following command in Ignite project root folder:
+  mvn clean package -pl :ignite-web-agent -am -P web-console -DskipTests=true
+
+Demo of Ignite Web Agent:
+ In order to simplify evaluation demo mode was implemented. To start demo, you need to click button "Start demo".
+ New tab will be open with prepared demo data.
+
+ 1) Demo for import domain model from database.
+   In this mode an in-memory H2 database will be started.
+   How to evaluate:
+     1.1) Go to Ignite Web Console "Domain model" screen.
+     1.2) Click "Import from database". You should see modal with demo description.
+     1.3) Click "Next" button. You should see list of available schemas.
+     1.4) Click "Next" button. You should see list of available tables.
+     1.5) Click "Next" button. You should see import options.
+     1.6) Select some of them and click "Save".
+
+   2) Demo for SQL.
+     How to evaluate:
+     In this mode internal Ignite node will be started. Cache created and populated with data.
+       2.1) Click "SQL" in Ignite Web Console top menu.
+       2.2) "Demo" notebook with preconfigured queries will be opened.
+       2.3) You can also execute any SQL queries for tables: "Country, Department, Employee, Parking, Car".
+
+ For example:
+   2.4) Enter SQL statement:
+           SELECT p.name, count(*) AS cnt FROM "ParkingCache".Parking p
+           INNER JOIN "CarCache".Car c ON (p.id) = (c.parkingId)
+           GROUP BY P.NAME
+   2.5) Click "Execute" button. You should get some data in table.
+   2.6) Click charts buttons to see auto generated charts.
diff --git a/modules/web-agent/assembly/release-web-agent.xml b/modules/web-agent/assembly/release-web-agent.xml
new file mode 100644
index 0000000..bb994c0
--- /dev/null
+++ b/modules/web-agent/assembly/release-web-agent.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<assembly xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
+          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
+    <id>release-ignite-web-agent</id>
+
+    <formats>
+        <format>zip</format>
+    </formats>
+
+    <fileSets>
+        <fileSet>
+            <directory>${basedir}</directory>
+            <outputDirectory>/</outputDirectory>
+            <includes>
+                <include>jdbc-drivers/README*</include>
+                <include>demo/README*</include>
+                <include>demo/*.sql</include>
+                <include>logs/README*</include>
+                <include>README*</include>
+                <include>LICENSE*</include>
+                <include>NOTICE*</include>
+            </includes>
+        </fileSet>
+        <fileSet>
+            <directory>${basedir}/bin</directory>
+            <outputDirectory>/</outputDirectory>
+            <includes>
+                <include>**/*.bat</include>
+            </includes>
+        </fileSet>
+        <fileSet>
+            <directory>${basedir}/bin</directory>
+            <outputDirectory>/</outputDirectory>
+            <fileMode>0755</fileMode>
+            <includes>
+                <include>**/*.sh</include>
+            </includes>
+        </fileSet>
+        <fileSet>
+            <directory>${project.build.directory}</directory>
+            <outputDirectory>/</outputDirectory>
+            <includes>
+                <include>ignite-web-agent-${project.version}.jar</include>
+            </includes>
+        </fileSet>
+    </fileSets>
+</assembly>
diff --git a/modules/web-agent/bin/ignite-web-agent.bat b/modules/web-agent/bin/ignite-web-agent.bat
new file mode 100644
index 0000000..3039543
--- /dev/null
+++ b/modules/web-agent/bin/ignite-web-agent.bat
@@ -0,0 +1,143 @@
+::
+:: Licensed to the Apache Software Foundation (ASF) under one or more
+:: contributor license agreements.  See the NOTICE file distributed with
+:: this work for additional information regarding copyright ownership.
+:: The ASF licenses this file to You under the Apache License, Version 2.0
+:: (the "License"); you may not use this file except in compliance with
+:: the License.  You may obtain a copy of the License at
+::
+::      http://www.apache.org/licenses/LICENSE-2.0
+::
+:: Unless required by applicable law or agreed to in writing, software
+:: distributed under the License is distributed on an "AS IS" BASIS,
+:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+:: See the License for the specific language governing permissions and
+:: limitations under the License.
+::
+
+@echo off
+Setlocal EnableDelayedExpansion
+
+if "%OS%" == "Windows_NT"  setlocal
+
+:: Check IGNITE_HOME.
+pushd "%~dp0"
+set IGNITE_HOME=%CD%
+
+:checkIgniteHome2
+:: Strip double quotes from IGNITE_HOME
+set IGNITE_HOME=%IGNITE_HOME:"=%
+
+:: remove all trailing slashes from IGNITE_HOME.
+if %IGNITE_HOME:~-1,1% == \ goto removeTrailingSlash
+if %IGNITE_HOME:~-1,1% == / goto removeTrailingSlash
+goto checkIgniteHome3
+
+:removeTrailingSlash
+set IGNITE_HOME=%IGNITE_HOME:~0,-1%
+goto checkIgniteHome2
+
+:checkIgniteHome3
+
+:: Check JAVA_HOME.
+if defined JAVA_HOME  goto checkJdk
+    echo %0, ERROR:
+    echo JAVA_HOME environment variable is not found.
+    echo Please point JAVA_HOME variable to location of JDK 1.8 or later.
+    echo You can also download latest JDK at http://java.com/download.
+goto error_finish
+
+:checkJdk
+:: Check that JDK is where it should be.
+if exist "%JAVA_HOME%\bin\java.exe" goto checkJdkVersion
+    echo %0, ERROR:
+    echo JAVA is not found in JAVA_HOME=%JAVA_HOME%.
+    echo Please point JAVA_HOME variable to installation of JDK 1.8 or later.
+    echo You can also download latest JDK at http://java.com/download.
+goto error_finish
+
+:checkJdkVersion
+set cmd="%JAVA_HOME%\bin\java.exe"
+for /f "tokens=* USEBACKQ" %%f in (`%cmd% -version 2^>^&1`) do (
+    set var=%%f
+    goto :LoopEscape
+)
+:LoopEscape
+
+for /f "tokens=1-3  delims= " %%a in ("%var%") do set JAVA_VER_STR=%%c
+set JAVA_VER_STR=%JAVA_VER_STR:"=%
+
+for /f "tokens=1,2 delims=." %%a in ("%JAVA_VER_STR%.x") do set MAJOR_JAVA_VER=%%a & set MINOR_JAVA_VER=%%b
+if %MAJOR_JAVA_VER% == 1 set MAJOR_JAVA_VER=%MINOR_JAVA_VER%
+
+if %MAJOR_JAVA_VER% LSS 8 (
+    echo %0, ERROR:
+    echo The version of JAVA installed in %JAVA_HOME% is incorrect.
+    echo Please point JAVA_HOME variable to installation of JDK 1.8 or later.
+    echo You can also download latest JDK at http://java.com/download.
+	goto error_finish
+)
+
+:run_java
+
+::
+:: JVM options. See http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp for more details.
+::
+:: ADD YOUR/CHANGE ADDITIONAL OPTIONS HERE
+::
+"%JAVA_HOME%\bin\java.exe" -version 2>&1 | findstr "1\.[7]\." > nul
+if %ERRORLEVEL% equ 0 (
+    if "%JVM_OPTS%" == "" set JVM_OPTS=-Xms1g -Xmx1g -server -XX:MaxPermSize=256m
+) else (
+    if "%JVM_OPTS%" == "" set JVM_OPTS=-Xms1g -Xmx1g -server -XX:MaxMetaspaceSize=256m
+)
+
+:: https://confluence.atlassian.com/kb/basic-authentication-fails-for-outgoing-proxy-in-java-8u111-909643110.html
+set JVM_OPTS=%JVM_OPTS% -Djava.net.useSystemProxies=true -Djdk.http.auth.tunneling.disabledSchemes=
+
+::
+:: Final JVM_OPTS for Java 9+ compatibility
+::
+if "%MAJOR_JAVA_VER%" == "8" (
+    set JVM_OPTS= ^
+    -XX:+AggressiveOpts ^
+    %JVM_OPTS%
+)
+
+if %MAJOR_JAVA_VER% GEQ 9 if %MAJOR_JAVA_VER% LSS 11 (
+    set JVM_OPTS= ^
+    -XX:+AggressiveOpts ^
+    --add-exports=java.base/jdk.internal.misc=ALL-UNNAMED ^
+    --add-exports=java.base/sun.nio.ch=ALL-UNNAMED ^
+    --add-exports=java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED ^
+    --add-exports=jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED ^
+    --add-exports=java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED ^
+    --illegal-access=permit ^
+    --add-modules=java.xml.bind ^
+    %JVM_OPTS%
+)
+
+if "%MAJOR_JAVA_VER%" GEQ "11" (
+    set JVM_OPTS= ^
+    --add-exports=java.base/jdk.internal.misc=ALL-UNNAMED ^
+    --add-exports=java.base/sun.nio.ch=ALL-UNNAMED ^
+    --add-exports=java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED ^
+    --add-exports=jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED ^
+    --add-exports=java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED ^
+    --illegal-access=permit ^
+    %JVM_OPTS%
+)
+
+"%JAVA_HOME%\bin\java.exe" %JVM_OPTS% -cp "*" org.apache.ignite.console.agent.AgentLauncher %*
+
+set JAVA_ERRORLEVEL=%ERRORLEVEL%
+
+:: errorlevel 130 if aborted with Ctrl+c
+if %JAVA_ERRORLEVEL%==130 goto eof
+
+:error_finish
+
+if not "%NO_PAUSE%" == "1" pause
+
+goto :eof
+
diff --git a/modules/web-agent/bin/ignite-web-agent.sh b/modules/web-agent/bin/ignite-web-agent.sh
new file mode 100755
index 0000000..1df6616
--- /dev/null
+++ b/modules/web-agent/bin/ignite-web-agent.sh
@@ -0,0 +1,91 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+SOURCE="${BASH_SOURCE[0]}"
+
+# Resolve $SOURCE until the file is no longer a symlink.
+while [ -h "$SOURCE" ]
+    do
+        IGNITE_HOME="$(cd -P "$( dirname "$SOURCE"  )" && pwd)"
+
+        SOURCE="$(readlink "$SOURCE")"
+
+        # If $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located.
+        [[ $SOURCE != /* ]] && SOURCE="$IGNITE_HOME/$SOURCE"
+    done
+
+#
+# Set IGNITE_HOME.
+#
+export IGNITE_HOME="$(cd -P "$( dirname "$SOURCE" )" && pwd)"
+
+source "${IGNITE_HOME}"/include/functions.sh
+
+#
+# Discover path to Java executable and check it's version.
+#
+checkJava
+
+#
+# JVM options. See http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp for more details.
+#
+# ADD YOUR/CHANGE ADDITIONAL OPTIONS HERE
+#
+if [ -z "$JVM_OPTS" ] ; then
+    if [[ `"$JAVA" -version 2>&1 | egrep "1\.[7]\."` ]]; then
+        JVM_OPTS="-Xms1g -Xmx1g -server -XX:MaxPermSize=256m"
+    else
+        JVM_OPTS="-Xms1g -Xmx1g -server -XX:MaxMetaspaceSize=256m"
+    fi
+fi
+
+# https://confluence.atlassian.com/kb/basic-authentication-fails-for-outgoing-proxy-in-java-8u111-909643110.html
+JVM_OPTS="${JVM_OPTS} -Djava.net.useSystemProxies=true -Djdk.http.auth.tunneling.disabledSchemes="
+
+#
+# Final JVM_OPTS for Java 9+ compatibility
+#
+if [ $version -eq 8 ] ; then
+    JVM_OPTS="\
+        -XX:+AggressiveOpts \
+         ${JVM_OPTS}"
+
+elif [ $version -gt 8 ] && [ $version -lt 11 ]; then
+    JVM_OPTS="\
+        -XX:+AggressiveOpts \
+        --add-exports=java.base/jdk.internal.misc=ALL-UNNAMED \
+        --add-exports=java.base/sun.nio.ch=ALL-UNNAMED \
+        --add-exports=java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED \
+        --add-exports=jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED \
+        --add-exports=java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED \
+        --illegal-access=permit \
+        --add-modules=java.xml.bind \
+        ${JVM_OPTS}"
+
+elif [ $version -ge 11 ] ; then
+    JVM_OPTS="\
+        --add-exports=java.base/jdk.internal.misc=ALL-UNNAMED \
+        --add-exports=java.base/sun.nio.ch=ALL-UNNAMED \
+        --add-exports=java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED \
+        --add-exports=jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED \
+        --add-exports=java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED \
+        --illegal-access=permit \
+        ${JVM_OPTS}"
+fi
+
+"$JAVA" ${JVM_OPTS} -cp "${IGNITE_HOME}/*" org.apache.ignite.console.agent.AgentLauncher "$@"
diff --git a/modules/web-agent/bin/include/functions.sh b/modules/web-agent/bin/include/functions.sh
new file mode 100644
index 0000000..ef69b75
--- /dev/null
+++ b/modules/web-agent/bin/include/functions.sh
@@ -0,0 +1,83 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# This is a collection of utility functions to be used in other Ignite scripts.
+# Before calling any function from this file you have to import it:
+#   if [ "${IGNITE_HOME}" = "" ];
+#       then IGNITE_HOME_TMP="$(dirname "$(cd "$(dirname "$0")"; "pwd")")";
+#       else IGNITE_HOME_TMP=${IGNITE_HOME};
+#   fi
+#
+#   source "${IGNITE_HOME_TMP}"/bin/include/functions.sh
+#
+
+# Extract java version to `version` variable.
+javaVersion() {
+    version=$("$1" -version 2>&1 | awk -F[\"\-] '/version/ {print $2}')
+}
+
+# Extract only major version of java to `version` variable.
+javaMajorVersion() {
+    javaVersion "$1"
+    version="${version%%.*}"
+
+    if [ ${version} -eq 1 ]; then
+        # Version seems starts from 1, we need second number.
+        javaVersion "$1"
+        version=$(awk -F[\"\.] '{print $2}' <<< ${version})
+    fi
+}
+
+#
+# Discovers path to Java executable and checks it's version.
+# The function exports JAVA variable with path to Java executable.
+#
+checkJava() {
+    # Check JAVA_HOME.
+    if [ "$JAVA_HOME" = "" ]; then
+        JAVA=`type -p java`
+        RETCODE=$?
+
+        if [ $RETCODE -ne 0 ]; then
+            echo $0", ERROR:"
+            echo "JAVA_HOME environment variable is not found."
+            echo "Please point JAVA_HOME variable to location of JDK 1.8 or later."
+            echo "You can also download latest JDK at http://java.com/download"
+
+            exit 1
+        fi
+
+        JAVA_HOME=
+    else
+        JAVA=${JAVA_HOME}/bin/java
+    fi
+
+    #
+    # Check JDK.
+    #
+    javaMajorVersion "$JAVA"
+
+    if [ $version -lt 8 ]; then
+        echo "$0, ERROR:"
+        echo "The $version version of JAVA installed in JAVA_HOME=$JAVA_HOME is incompatible."
+        echo "Please point JAVA_HOME variable to installation of JDK 1.8 or later."
+        echo "You can also download latest JDK at http://java.com/download"
+        exit 1
+    fi
+}
diff --git a/modules/web-agent/demo/README.txt b/modules/web-agent/demo/README.txt
new file mode 100644
index 0000000..17e5074
--- /dev/null
+++ b/modules/web-agent/demo/README.txt
@@ -0,0 +1,4 @@
+Ignite Web Agent
+======================================
+
+This is folder for demo files.
diff --git a/modules/web-agent/demo/db-init.sql b/modules/web-agent/demo/db-init.sql
new file mode 100644
index 0000000..0688ea0
--- /dev/null
+++ b/modules/web-agent/demo/db-init.sql
@@ -0,0 +1,102 @@
+--
+--  Licensed to the Apache Software Foundation (ASF) under one or more
+--  contributor license agreements.  See the NOTICE file distributed with
+--  this work for additional information regarding copyright ownership.
+--  The ASF licenses this file to You under the Apache License, Version 2.0
+--  (the "License"); you may not use this file except in compliance with
+--  the License.  You may obtain a copy of the License at
+--
+--       http://www.apache.org/licenses/LICENSE-2.0
+--
+--  Unless required by applicable law or agreed to in writing, software
+--  distributed under the License is distributed on an "AS IS" BASIS,
+--  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+--  See the License for the specific language governing permissions and
+--  limitations under the License.
+--
+
+CREATE TABLE COUNTRY (
+    ID         INTEGER NOT NULL PRIMARY KEY,
+    NAME       VARCHAR(50),
+    POPULATION INTEGER NOT NULL
+);
+
+CREATE TABLE DEPARTMENT (
+    ID         INTEGER NOT NULL PRIMARY KEY,
+    COUNTRY_ID INTEGER NOT NULL,
+    NAME       VARCHAR(50) NOT NULL
+);
+
+CREATE TABLE EMPLOYEE (
+    ID            INTEGER NOT NULL PRIMARY KEY,
+    DEPARTMENT_ID INTEGER NOT NULL,
+    MANAGER_ID    INTEGER,
+    FIRST_NAME    VARCHAR(50) NOT NULL,
+    LAST_NAME     VARCHAR(50) NOT NULL,
+    EMAIL         VARCHAR(50) NOT NULL,
+    PHONE_NUMBER  VARCHAR(50),
+    HIRE_DATE     DATE        NOT NULL,
+    JOB           VARCHAR(50) NOT NULL,
+    SALARY        DOUBLE
+);
+
+CREATE INDEX EMP_SALARY ON EMPLOYEE (SALARY ASC);
+CREATE INDEX EMP_NAMES ON EMPLOYEE (FIRST_NAME ASC, LAST_NAME ASC);
+
+CREATE SCHEMA CARS;
+
+CREATE TABLE CARS.PARKING (
+    ID       INTEGER     NOT NULL PRIMARY KEY,
+    NAME     VARCHAR(50) NOT NULL,
+    CAPACITY INTEGER NOT NULL
+);
+
+CREATE TABLE CARS.CAR (
+    ID         INTEGER NOT NULL PRIMARY KEY,
+    PARKING_ID INTEGER NOT NULL,
+    NAME       VARCHAR(50) NOT NULL
+);
+
+INSERT INTO COUNTRY(ID, NAME, POPULATION) VALUES(0, 'Country #1', 10000000);
+INSERT INTO COUNTRY(ID, NAME, POPULATION) VALUES(1, 'Country #2', 20000000);
+INSERT INTO COUNTRY(ID, NAME, POPULATION) VALUES(2, 'Country #3', 30000000);
+
+INSERT INTO DEPARTMENT(ID, COUNTRY_ID, NAME) VALUES(0, 0, 'Department #1');
+INSERT INTO DEPARTMENT(ID, COUNTRY_ID, NAME) VALUES(1, 0, 'Department #2');
+INSERT INTO DEPARTMENT(ID, COUNTRY_ID, NAME) VALUES(2, 2, 'Department #3');
+INSERT INTO DEPARTMENT(ID, COUNTRY_ID, NAME) VALUES(3, 1, 'Department #4');
+INSERT INTO DEPARTMENT(ID, COUNTRY_ID, NAME) VALUES(4, 1, 'Department #5');
+INSERT INTO DEPARTMENT(ID, COUNTRY_ID, NAME) VALUES(5, 1, 'Department #6');
+
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(0, 0, 'First name manager #1', 'Last name manager #1', 'Email manager #1', 'Phone number manager #1', '2014-01-01', 'Job manager #1', 1100.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(1, 1, 'First name manager #2', 'Last name manager #2', 'Email manager #2', 'Phone number manager #2', '2014-01-01', 'Job manager #2', 2100.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(2, 2, 'First name manager #3', 'Last name manager #3', 'Email manager #3', 'Phone number manager #3', '2014-01-01', 'Job manager #3', 3100.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(3, 3, 'First name manager #4', 'Last name manager #4', 'Email manager #4', 'Phone number manager #4', '2014-01-01', 'Job manager #4', 1500.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(4, 4, 'First name manager #5', 'Last name manager #5', 'Email manager #5', 'Phone number manager #5', '2014-01-01', 'Job manager #5', 1700.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(5, 5, 'First name manager #6', 'Last name manager #6', 'Email manager #6', 'Phone number manager #6', '2014-01-01', 'Job manager #6', 1300.00);
+
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, MANAGER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(101, 0, 0, 'First name employee #1', 'Last name employee #1', 'Email employee #1', 'Phone number employee #1', '2014-01-01', 'Job employee #1', 600.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, MANAGER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(102, 0, 0, 'First name employee #2', 'Last name employee #2', 'Email employee #2', 'Phone number employee #2', '2014-01-01', 'Job employee #2', 1600.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, MANAGER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(103, 1, 1, 'First name employee #3', 'Last name employee #3', 'Email employee #3', 'Phone number employee #3', '2014-01-01', 'Job employee #3', 2600.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, MANAGER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(104, 2, 2, 'First name employee #4', 'Last name employee #4', 'Email employee #4', 'Phone number employee #4', '2014-01-01', 'Job employee #4', 1000.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, MANAGER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(105, 2, 2, 'First name employee #5', 'Last name employee #5', 'Email employee #5', 'Phone number employee #5', '2014-01-01', 'Job employee #5', 1200.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, MANAGER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(106, 2, 2, 'First name employee #6', 'Last name employee #6', 'Email employee #6', 'Phone number employee #6', '2014-01-01', 'Job employee #6', 800.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, MANAGER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(107, 3, 3, 'First name employee #7', 'Last name employee #7', 'Email employee #7', 'Phone number employee #7', '2014-01-01', 'Job employee #7', 1400.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, MANAGER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(108, 4, 4, 'First name employee #8', 'Last name employee #8', 'Email employee #8', 'Phone number employee #8', '2014-01-01', 'Job employee #8', 800.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, MANAGER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(109, 4, 4, 'First name employee #9', 'Last name employee #9', 'Email employee #9', 'Phone number employee #9', '2014-01-01', 'Job employee #9', 1490.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, MANAGER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(110, 4, 4, 'First name employee #10', 'Last name employee #12', 'Email employee #10', 'Phone number employee #10', '2014-01-01', 'Job employee #10', 1600.00);
+INSERT INTO EMPLOYEE(ID, DEPARTMENT_ID, MANAGER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB, SALARY) VALUES(111, 5, 5, 'First name employee #11', 'Last name employee #11', 'Email employee #11', 'Phone number employee #11', '2014-01-01', 'Job employee #11', 400.00);
+
+INSERT INTO CARS.PARKING(ID, NAME, CAPACITY) VALUES(0, 'Parking #1', 10);
+INSERT INTO CARS.PARKING(ID, NAME, CAPACITY) VALUES(1, 'Parking #2', 20);
+INSERT INTO CARS.PARKING(ID, NAME, CAPACITY) VALUES(2, 'Parking #3', 30);
+
+INSERT INTO CARS.CAR(ID, PARKING_ID, NAME) VALUES(0, 0, 'Car #1');
+INSERT INTO CARS.CAR(ID, PARKING_ID, NAME) VALUES(1, 0, 'Car #2');
+INSERT INTO CARS.CAR(ID, PARKING_ID, NAME) VALUES(2, 0, 'Car #3');
+INSERT INTO CARS.CAR(ID, PARKING_ID, NAME) VALUES(3, 1, 'Car #4');
+INSERT INTO CARS.CAR(ID, PARKING_ID, NAME) VALUES(4, 1, 'Car #5');
+INSERT INTO CARS.CAR(ID, PARKING_ID, NAME) VALUES(5, 2, 'Car #6');
+INSERT INTO CARS.CAR(ID, PARKING_ID, NAME) VALUES(6, 2, 'Car #7');
+INSERT INTO CARS.CAR(ID, PARKING_ID, NAME) VALUES(7, 2, 'Car #8');
+INSERT INTO CARS.CAR(ID, PARKING_ID, NAME) VALUES(8, 2, 'Car #9');
diff --git a/modules/web-agent/jdbc-drivers/README.txt b/modules/web-agent/jdbc-drivers/README.txt
new file mode 100644
index 0000000..cad43b7
--- /dev/null
+++ b/modules/web-agent/jdbc-drivers/README.txt
@@ -0,0 +1,10 @@
+Ignite Web Agent
+======================================
+
+If you are are planning to load cache type metadata from your existing databases
+you need to copy JDBC drivers in this folder.
+
+This is default folder for JDBC drivers.
+
+Also, you could specify custom folder using option: "-d CUSTOM_PATH_TO_FOLDER_WITH_JDBC_DRIVERS".
+
diff --git a/modules/web-agent/logs/README.txt b/modules/web-agent/logs/README.txt
new file mode 100644
index 0000000..3a220eb
--- /dev/null
+++ b/modules/web-agent/logs/README.txt
@@ -0,0 +1,5 @@
+Ignite Web Agent
+======================================
+
+This is folder for agent logs.
+
diff --git a/modules/web-agent/pom.xml b/modules/web-agent/pom.xml
new file mode 100644
index 0000000..bf6b9c1
--- /dev/null
+++ b/modules/web-agent/pom.xml
@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<!--
+    POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.ignite</groupId>
+        <artifactId>ignite-parent</artifactId>
+        <version>1</version>
+        <relativePath>../../parent</relativePath>
+    </parent>
+
+    <artifactId>ignite-web-agent</artifactId>
+    <packaging>jar</packaging>
+    <version>2.10.0-SNAPSHOT</version>
+    <url>http://ignite.apache.org</url>
+
+    <properties>
+        <maven.build.timestamp.format>yyMMddHHmmss</maven.build.timestamp.format>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.socket</groupId>
+            <artifactId>socket.io-client</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-json-org</artifactId>
+            <version>${jackson.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.beust</groupId>
+            <artifactId>jcommander</artifactId>
+            <version>1.58</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>3.12.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-indexing</artifactId>
+            <version>${ignite.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-rest-http</artifactId>
+            <version>${ignite.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-spring</artifactId>
+            <version>${ignite.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-aop</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-tx</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-jdbc</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-slf4j</artifactId>
+            <version>${ignite.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jul-to-slf4j</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>ignite-web-agent-${project.version}</finalName>
+
+        <testResources>
+            <testResource>
+                <directory>src/test/java</directory>
+                <excludes>
+                    <exclude>**/*.java</exclude>
+                </excludes>
+            </testResource>
+            <testResource>
+                <directory>src/test/resources</directory>
+            </testResource>
+        </testResources>
+
+        <plugins>
+            <plugin>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>2.5</version>
+
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>org.apache.ignite.console.agent.AgentLauncher</mainClass>
+                        </manifest>
+                        <manifestEntries>
+                            <Build-Time>${maven.build.timestamp}</Build-Time>
+                        </manifestEntries>
+                    </archive>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>2.4</version>
+
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+
+                        <configuration>
+                            <createDependencyReducedPom>false</createDependencyReducedPom>
+                            <filters>
+                                <filter>
+                                    <artifact>*:*</artifact>
+                                    <excludes>
+                                        <exclude>META-INF/maven/**</exclude>
+                                    </excludes>
+                                </filter>
+                            </filters>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <version>2.4</version>
+                <inherited>false</inherited>
+
+                <executions>
+                    <execution>
+                        <id>release-web-agent</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <configuration>
+                            <descriptors>
+                                <descriptor>assembly/release-web-agent.xml</descriptor>
+                            </descriptors>
+                            <finalName>ignite-web-agent-${ignite.version}</finalName>
+                            <outputDirectory>target</outputDirectory>
+                            <appendAssemblyId>false</appendAssemblyId>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <skip>true</skip>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-deploy-plugin</artifactId>
+                <configuration>
+                    <skip>true</skip>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/AgentConfiguration.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/AgentConfiguration.java
new file mode 100644
index 0000000..6eed517
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/AgentConfiguration.java
@@ -0,0 +1,619 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+import java.util.stream.Collectors;
+import com.beust.jcommander.Parameter;
+import org.apache.ignite.internal.util.typedef.F;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Agent configuration.
+ */
+public class AgentConfiguration {
+    /** Default path to agent property file. */
+    public static final String DFLT_CFG_PATH = "default.properties";
+
+    /** Default server URI. */
+    private static final String DFLT_SERVER_URI = "http://localhost:3000";
+
+    /** Default Ignite node HTTP URI. */
+    private static final String DFLT_NODE_URI = "http://localhost:8080";
+
+    /** */
+    @Parameter(names = {"-t", "--tokens"},
+        description = "User's tokens separated by comma used to connect to Ignite Console.")
+    private List<String> tokens;
+
+    /** */
+    @Parameter(names = {"-s", "--server-uri"},
+        description = "URI for connect to Ignite Console via web-socket protocol" +
+            "           " +
+            "      Default value: " + DFLT_SERVER_URI)
+    private String srvUri;
+
+    /** */
+    @Parameter(names = {"-n", "--node-uri"},
+        description = "Comma-separated list of URIs for connect to Ignite node via REST" +
+            "                        " +
+            "      Default value: " + DFLT_NODE_URI)
+    private List<String> nodeURIs;
+
+    /** */
+    @Parameter(names = {"-nl", "--node-login"},
+        description = "User name that will be used to connect to secured cluster")
+    private String nodeLogin;
+
+    /** */
+    @Parameter(names = {"-np", "--node-password"},
+        description = "Password that will be used to connect to secured cluster")
+    private String nodePwd;
+
+    /** URI for connect to Ignite demo node REST server */
+    private String demoNodeUri;
+
+    /** */
+    @Parameter(names = {"-c", "--config"}, description = "Path to agent property file" +
+        "                                  " +
+        "      Default value: " + DFLT_CFG_PATH)
+    private String cfgPath;
+
+    /** */
+    @Parameter(names = {"-d", "--driver-folder"}, description = "Path to folder with JDBC drivers" +
+        "                             " +
+        "      Default value: ./jdbc-drivers")
+    private String driversFolder;
+
+    /** */
+    @Parameter(names = {"-dd", "--disable-demo"}, description = "Disable demo mode on this agent " +
+        "                             " +
+        "      Default value: false")
+    private Boolean disableDemo;
+
+    /** */
+    @Parameter(names = {"-nks", "--node-key-store"},
+        description = "Path to key store that will be used to connect to cluster")
+    private String nodeKeyStore;
+
+    /** */
+    @Parameter(names = {"-nksp", "--node-key-store-password"},
+        description = "Optional password for node key store")
+    private String nodeKeyStorePass;
+
+    /** */
+    @Parameter(names = {"-nts", "--node-trust-store"},
+        description = "Path to trust store that will be used to connect to cluster")
+    private String nodeTrustStore;
+
+    /** */
+    @Parameter(names = {"-ntsp", "--node-trust-store-password"},
+        description = "Optional password for node trust store")
+    private String nodeTrustStorePass;
+
+    /** */
+    @Parameter(names = {"-sks", "--server-key-store"},
+        description = "Path to key store that will be used to connect to Web server")
+    private String srvKeyStore;
+
+    /** */
+    @Parameter(names = {"-sksp", "--server-key-store-password"},
+        description = "Optional password for server key store")
+    private String srvKeyStorePass;
+
+    /** */
+    @Parameter(names = {"-sts", "--server-trust-store"},
+        description = "Path to trust store that will be used to connect to Web server")
+    private String srvTrustStore;
+
+    /** */
+    @Parameter(names = {"-stsp", "--server-trust-store-password"},
+        description = "Optional password for server trust store")
+    private String srvTrustStorePass;
+
+    /** */
+    @Parameter(names = {"-cs", "--cipher-suites"},
+        description = "Optional comma-separated list of SSL cipher suites to be used to connect to server and cluster")
+    private List<String> cipherSuites;
+
+    /** */
+    @Parameter(names = {"-h", "--help"}, help = true, description = "Print this help message")
+    private Boolean help;
+
+    /**
+     * @return Tokens.
+     */
+    public List<String> tokens() {
+        return tokens;
+    }
+
+    /**
+     * @param tokens Tokens.
+     */
+    public void tokens(List<String> tokens) {
+        this.tokens = tokens;
+    }
+
+    /**
+     * @return Server URI.
+     */
+    public String serverUri() {
+        return srvUri;
+    }
+
+    /**
+     * @param srvUri URI.
+     */
+    public void serverUri(String srvUri) {
+        this.srvUri = srvUri;
+    }
+
+    /**
+     * @return Node URIs.
+     */
+    public List<String> nodeURIs() {
+        return nodeURIs;
+    }
+
+    /**
+     * @param nodeURIs Node URIs.
+     */
+    public void nodeURIs(List<String> nodeURIs) {
+        this.nodeURIs = nodeURIs;
+    }
+
+    /**
+     * @return User name for agent to authenticate on node.
+     */
+    public String nodeLogin() {
+        return nodeLogin;
+    }
+
+    /**
+     * @param nodeLogin User name for agent to authenticate on node.
+     */
+    public void nodeLogin(String nodeLogin) {
+        this.nodeLogin = nodeLogin;
+    }
+
+    /**
+     * @return Agent password to authenticate on node.
+     */
+    public String nodePassword() {
+        return nodePwd;
+    }
+
+    /**
+     * @param nodePwd Agent password to authenticate on node.
+     */
+    public void nodePassword(String nodePwd) {
+        this.nodePwd = nodePwd;
+    }
+
+    /**
+     * @return Demo node URI.
+     */
+    public String demoNodeUri() {
+        return demoNodeUri;
+    }
+
+    /**
+     * @param demoNodeUri Demo node URI.
+     */
+    public void demoNodeUri(String demoNodeUri) {
+        this.demoNodeUri = demoNodeUri;
+    }
+
+    /**
+     * @return Configuration path.
+     */
+    public String configPath() {
+        return cfgPath == null ? DFLT_CFG_PATH : cfgPath;
+    }
+
+    /**
+     * @return Configured drivers folder.
+     */
+    public String driversFolder() {
+        return driversFolder;
+    }
+
+    /**
+     * @param driversFolder Driver folder.
+     */
+    public void driversFolder(String driversFolder) {
+        this.driversFolder = driversFolder;
+    }
+
+    /**
+     * @return Disable demo mode.
+     */
+    public Boolean disableDemo() {
+        return disableDemo != null ? disableDemo : Boolean.FALSE;
+    }
+
+    /**
+     * @param disableDemo Disable demo mode.
+     */
+    public void disableDemo(Boolean disableDemo) {
+        this.disableDemo = disableDemo;
+    }
+
+    /**
+     * @return Path to node key store.
+     */
+    public String nodeKeyStore() {
+        return nodeKeyStore;
+    }
+
+    /**
+     * @param nodeKeyStore Path to node key store.
+     */
+    public void nodeKeyStore(String nodeKeyStore) {
+        this.nodeKeyStore = nodeKeyStore;
+    }
+
+    /**
+     * @return Node key store password.
+     */
+    public String nodeKeyStorePassword() {
+        return nodeKeyStorePass;
+    }
+
+    /**
+     * @param nodeKeyStorePass Node key store password.
+     */
+    public void nodeKeyStorePassword(String nodeKeyStorePass) {
+        this.nodeKeyStorePass = nodeKeyStorePass;
+    }
+
+    /**
+     * @return Path to node trust store.
+     */
+    public String nodeTrustStore() {
+        return nodeTrustStore;
+    }
+
+    /**
+     * @param nodeTrustStore Path to node trust store.
+     */
+    public void nodeTrustStore(String nodeTrustStore) {
+        this.nodeTrustStore = nodeTrustStore;
+    }
+
+    /**
+     * @return Node trust store password.
+     */
+    public String nodeTrustStorePassword() {
+        return nodeTrustStorePass;
+    }
+
+    /**
+     * @param nodeTrustStorePass Node trust store password.
+     */
+    public void nodeTrustStorePassword(String nodeTrustStorePass) {
+        this.nodeTrustStorePass = nodeTrustStorePass;
+    }
+
+    /**
+     * @return Path to server key store.
+     */
+    public String serverKeyStore() {
+        return srvKeyStore;
+    }
+
+    /**
+     * @param srvKeyStore Path to server key store.
+     */
+    public void serverKeyStore(String srvKeyStore) {
+        this.srvKeyStore = srvKeyStore;
+    }
+
+    /**
+     * @return Server key store password.
+     */
+    public String serverKeyStorePassword() {
+        return srvKeyStorePass;
+    }
+
+    /**
+     * @param srvKeyStorePass Server key store password.
+     */
+    public void serverKeyStorePassword(String srvKeyStorePass) {
+        this.srvKeyStorePass = srvKeyStorePass;
+    }
+
+    /**
+     * @return Path to server trust store.
+     */
+    public String serverTrustStore() {
+        return srvTrustStore;
+    }
+
+    /**
+     * @param srvTrustStore Path to server trust store.
+     */
+    public void serverTrustStore(String srvTrustStore) {
+        this.srvTrustStore = srvTrustStore;
+    }
+
+    /**
+     * @return Server trust store password.
+     */
+    public String serverTrustStorePassword() {
+        return srvTrustStorePass;
+    }
+
+    /**
+     * @param srvTrustStorePass Server trust store password.
+     */
+    public void serverTrustStorePassword(String srvTrustStorePass) {
+        this.srvTrustStorePass = srvTrustStorePass;
+    }
+
+    /**
+     * @return SSL cipher suites.
+     */
+    public List<String> cipherSuites() {
+        return cipherSuites;
+    }
+
+    /**
+     * @param cipherSuites SSL cipher suites.
+     */
+    public void cipherSuites(List<String> cipherSuites) {
+        this.cipherSuites = cipherSuites;
+    }
+
+    /**
+     * @return {@code true} If agent options usage should be printed.
+     */
+    public Boolean help() {
+        return help != null ? help : Boolean.FALSE;
+    }
+
+    /**
+     * @param cfgUrl URL.
+     */
+    public void load(URL cfgUrl) throws IOException {
+        Properties props = new Properties();
+
+        try (Reader reader = new InputStreamReader(cfgUrl.openStream(), UTF_8)) {
+            props.load(reader);
+        }
+
+        String val = props.getProperty("tokens");
+
+        if (val != null)
+            tokens(new ArrayList<>(Arrays.asList(val.split(","))));
+
+        val = props.getProperty("server-uri");
+
+        if (val != null)
+            serverUri(val);
+
+        val = props.getProperty("node-uri");
+
+        // Intentionaly wrapped by ArrayList, for further maniulations.
+        if (val != null)
+            nodeURIs(new ArrayList<>(Arrays.asList(val.split(","))));
+
+        val = props.getProperty("node-login");
+
+        if (val != null)
+            nodeLogin(val);
+
+        val = props.getProperty("node-password");
+
+        if (val != null)
+            nodePassword(val);
+
+        val = props.getProperty("driver-folder");
+
+        if (val != null)
+            driversFolder(val);
+
+        val = props.getProperty("node-key-store");
+
+        if (val != null)
+            nodeKeyStore(val);
+
+        val = props.getProperty("node-key-store-password");
+
+        if (val != null)
+            nodeKeyStorePassword(val);
+
+        val = props.getProperty("node-trust-store");
+
+        if (val != null)
+            nodeTrustStore(val);
+
+        val = props.getProperty("node-trust-store-password");
+
+        if (val != null)
+            nodeTrustStorePassword(val);
+
+        val = props.getProperty("server-key-store");
+
+        if (val != null)
+            serverKeyStore(val);
+
+        val = props.getProperty("server-key-store-password");
+
+        if (val != null)
+            serverKeyStorePassword(val);
+
+        val = props.getProperty("server-trust-store");
+
+        if (val != null)
+            serverTrustStore(val);
+
+        val = props.getProperty("server-trust-store-password");
+
+        if (val != null)
+            serverTrustStorePassword(val);
+
+        val = props.getProperty("cipher-suites");
+
+        if (val != null)
+            cipherSuites(Arrays.asList(val.split(",")));
+    }
+
+    /**
+     * @param cfg Config to merge with.
+     */
+    public void merge(AgentConfiguration cfg) {
+        if (tokens == null)
+            tokens(cfg.tokens());
+
+        if (srvUri == null)
+            serverUri(cfg.serverUri());
+
+        if (srvUri == null)
+            serverUri(DFLT_SERVER_URI);
+
+        if (nodeURIs == null)
+            nodeURIs(cfg.nodeURIs());
+
+        if (nodeURIs == null)
+            nodeURIs(Collections.singletonList(DFLT_NODE_URI));
+
+        if (nodeLogin == null)
+            nodeLogin(cfg.nodeLogin());
+
+        if (nodePwd == null)
+            nodePassword(cfg.nodePassword());
+
+        if (driversFolder == null)
+            driversFolder(cfg.driversFolder());
+
+        if (disableDemo == null)
+            disableDemo(cfg.disableDemo());
+
+        if (nodeKeyStore == null)
+            nodeKeyStore(cfg.nodeKeyStore());
+
+        if (nodeKeyStorePass == null)
+            nodeKeyStorePassword(cfg.nodeKeyStorePassword());
+
+        if (nodeTrustStore == null)
+            nodeTrustStore(cfg.nodeTrustStore());
+
+        if (nodeTrustStorePass == null)
+            nodeTrustStorePassword(cfg.nodeTrustStorePassword());
+
+        if (srvKeyStore == null)
+            serverKeyStore(cfg.serverKeyStore());
+
+        if (srvKeyStorePass == null)
+            serverKeyStorePassword(cfg.serverKeyStorePassword());
+
+        if (srvTrustStore == null)
+            serverTrustStore(cfg.serverTrustStore());
+
+        if (srvTrustStorePass == null)
+            serverTrustStorePassword(cfg.serverTrustStorePassword());
+
+        if (cipherSuites == null)
+            cipherSuites(cfg.cipherSuites());
+    }
+
+    /**
+     * @param s String with sensitive data.
+     * @return Secured string.
+     */
+    private String secured(String s) {
+        int len = s.length();
+        int toShow = len > 4 ? 4 : 1;
+
+        return new String(new char[len - toShow]).replace('\0', '*') + s.substring(len - toShow, len);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        StringBuilder sb = new StringBuilder();
+
+        String nl = System.lineSeparator();
+
+        if (!F.isEmpty(tokens)) {
+            sb.append("User's security tokens          : ");
+
+            sb.append(tokens.stream().map(this::secured).collect(Collectors.joining(", "))).append(nl);
+        }
+
+        sb.append("URI to Ignite node REST server  : ")
+            .append(nodeURIs == null ? DFLT_NODE_URI : String.join(", ", nodeURIs)).append(nl);
+
+        if (nodeLogin != null)
+            sb.append("Login to Ignite node REST server: ").append(nodeLogin).append(nl);
+
+        sb.append("URI to Ignite Console server    : ").append(srvUri == null ? DFLT_SERVER_URI : srvUri).append(nl);
+        sb.append("Path to agent property file     : ").append(configPath()).append(nl);
+
+        String drvFld = driversFolder();
+
+        if (drvFld == null) {
+            File agentHome = AgentUtils.getAgentHome();
+
+            if (agentHome != null)
+                drvFld = new File(agentHome, "jdbc-drivers").getPath();
+        }
+
+        sb.append("Path to JDBC drivers folder     : ").append(drvFld).append(nl);
+        sb.append("Demo mode                       : ").append(disableDemo() ? "disabled" : "enabled").append(nl);
+
+        if (!F.isEmpty(nodeKeyStore))
+            sb.append("Node key store                  : ").append(nodeKeyStore).append(nl);
+
+        if (!F.isEmpty(nodeKeyStorePass))
+            sb.append("Node key store password         : ").append(secured(nodeKeyStorePass)).append(nl);
+
+        if (!F.isEmpty(nodeTrustStore))
+            sb.append("Node trust store                : ").append(nodeTrustStore).append(nl);
+
+        if (!F.isEmpty(nodeTrustStorePass))
+            sb.append("Node trust store password       : ").append(secured(nodeTrustStorePass)).append(nl);
+
+        if (!F.isEmpty(srvKeyStore))
+            sb.append("Server key store                : ").append(srvKeyStore).append(nl);
+
+        if (!F.isEmpty(srvKeyStorePass))
+            sb.append("Server key store password       : ").append(secured(srvKeyStorePass)).append(nl);
+
+        if (!F.isEmpty(srvTrustStore))
+            sb.append("Server trust store              : ").append(srvTrustStore).append(nl);
+
+        if (!F.isEmpty(srvTrustStorePass))
+            sb.append("Server trust store password     : ").append(secured(srvTrustStorePass)).append(nl);
+
+        if (!F.isEmpty(cipherSuites))
+            sb.append("Cipher suites                   : ").append(String.join(", ", cipherSuites)).append(nl);
+
+        return sb.toString();
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/AgentLauncher.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/AgentLauncher.java
new file mode 100644
index 0000000..951f3ce
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/AgentLauncher.java
@@ -0,0 +1,518 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.Authenticator;
+import java.net.ConnectException;
+import java.net.PasswordAuthentication;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Scanner;
+import java.util.concurrent.CountDownLatch;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.ParameterException;
+import io.socket.client.Ack;
+import io.socket.client.IO;
+import io.socket.client.Socket;
+import io.socket.emitter.Emitter;
+import okhttp3.OkHttpClient;
+import org.apache.ignite.console.agent.handlers.ClusterListener;
+import org.apache.ignite.console.agent.handlers.DatabaseListener;
+import org.apache.ignite.console.agent.handlers.RestListener;
+import org.apache.ignite.console.agent.rest.RestExecutor;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.X;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.bridge.SLF4JBridgeHandler;
+
+import static io.socket.client.Socket.EVENT_CONNECT;
+import static io.socket.client.Socket.EVENT_CONNECT_ERROR;
+import static io.socket.client.Socket.EVENT_DISCONNECT;
+import static io.socket.client.Socket.EVENT_ERROR;
+import static org.apache.ignite.console.agent.AgentUtils.fromJSON;
+import static org.apache.ignite.console.agent.AgentUtils.sslConnectionSpec;
+import static org.apache.ignite.console.agent.AgentUtils.sslSocketFactory;
+import static org.apache.ignite.console.agent.AgentUtils.toJSON;
+import static org.apache.ignite.console.agent.AgentUtils.trustManager;
+
+/**
+ * Ignite Web Agent launcher.
+ */
+public class AgentLauncher {
+    /** */
+    private static final Logger log = LoggerFactory.getLogger(AgentLauncher.class);
+
+    /** */
+    private static final String EVENT_SCHEMA_IMPORT_DRIVERS = "schemaImport:drivers";
+
+    /** */
+    private static final String EVENT_SCHEMA_IMPORT_SCHEMAS = "schemaImport:schemas";
+
+    /** */
+    private static final String EVENT_SCHEMA_IMPORT_METADATA = "schemaImport:metadata";
+
+    /** */
+    private static final String EVENT_NODE_VISOR_TASK = "node:visorTask";
+
+    /** */
+    private static final String EVENT_NODE_REST = "node:rest";
+
+    /** */
+    private static final String EVENT_RESET_TOKEN = "agent:reset:token";
+
+    /** */
+    private static final String EVENT_LOG_WARNING = "log:warn";
+
+    static {
+        // Optionally remove existing handlers attached to j.u.l root logger.
+        SLF4JBridgeHandler.removeHandlersForRootLogger();
+
+        // Add SLF4JBridgeHandler to j.u.l's root logger.
+        SLF4JBridgeHandler.install();
+    }
+
+    /**
+     * On error listener.
+     */
+    private static final Emitter.Listener onError = args -> {
+        Throwable e = (Throwable)args[0];
+
+        ConnectException ce = X.cause(e, ConnectException.class);
+
+        if (ce != null) {
+            log.error("Failed to establish connection to server or missing proxy settings (connection refused).");
+            log.error("Documentation for proxy configuration can be found here: https://apacheignite-tools.readme.io/docs/getting-started#section-proxy-configuration");
+        }
+        else {
+            if (X.hasCause(e, SSLHandshakeException.class)) {
+                log.error("Failed to establish SSL connection to server, due to errors with SSL handshake:", e);
+                log.error("Add to environment variable JVM_OPTS parameter \"-Dtrust.all=true\" to skip certificate validation in case of using self-signed certificate.");
+
+                System.exit(1);
+            }
+
+            if (X.hasCause(e, UnknownHostException.class)) {
+                log.error("Failed to establish connection to server, due to errors with DNS or missing proxy settings.", e);
+                log.error("Documentation for proxy configuration can be found here: https://apacheignite-tools.readme.io/docs/getting-started#section-proxy-configuration");
+
+                System.exit(1);
+            }
+
+            if (X.hasCause(e, ProxyAuthException.class)) {
+                log.error("Failed to establish connection to server, due to proxy requires authentication.");
+
+                String userName = System.getProperty("https.proxyUsername", System.getProperty("http.proxyUsername"));
+
+                if (userName == null || userName.trim().isEmpty())
+                    userName = readLine("Enter proxy user name: ");
+                else
+                    System.out.println("Read username from system properties: " + userName);
+
+                char[] pwd = readPassword("Enter proxy password: ");
+
+                final PasswordAuthentication pwdAuth = new PasswordAuthentication(userName, pwd);
+
+                Authenticator.setDefault(new Authenticator() {
+                    @Override protected PasswordAuthentication getPasswordAuthentication() {
+                        return pwdAuth;
+                    }
+                });
+
+                return;
+            }
+
+            IOException ignore = X.cause(e, IOException.class);
+
+            if (ignore != null && "404".equals(ignore.getMessage())) {
+                log.error("Failed to receive response from server (connection refused).");
+
+                return;
+            }
+
+            log.error("Connection error.", e);
+        }
+    };
+
+    /**
+     * On disconnect listener.
+     */
+    private static final Emitter.Listener onDisconnect = args -> log.error("Connection closed: {}", args);
+
+    /**
+     * On token reset listener.
+     */
+    private static final Emitter.Listener onLogWarning = args -> log.warn(String.valueOf(args[0]));
+
+    /**
+     * @param fmt Format string.
+     * @param args Arguments.
+     */
+    private static String readLine(String fmt, Object... args) {
+        if (System.console() != null)
+            return System.console().readLine(fmt, args);
+
+        System.out.print(String.format(fmt, args));
+
+        return new Scanner(System.in).nextLine();
+    }
+
+    /**
+     * @param fmt Format string.
+     * @param args Arguments.
+     */
+    private static char[] readPassword(String fmt, Object... args) {
+        if (System.console() != null)
+            return System.console().readPassword(fmt, args);
+
+        System.out.print(String.format(fmt, args));
+
+        return new Scanner(System.in).nextLine().toCharArray();
+    }
+
+    /**
+     * @param args Args.
+     */
+    public static void main(String[] args) throws Exception {
+        log.info("Starting Apache Ignite Web Console Agent...");
+
+        final AgentConfiguration cfg = new AgentConfiguration();
+
+        JCommander jCommander = new JCommander(cfg);
+
+        String osName = System.getProperty("os.name").toLowerCase();
+
+        jCommander.setProgramName("ignite-web-agent." + (osName.contains("win") ? "bat" : "sh"));
+
+        try {
+            jCommander.parse(args);
+        }
+        catch (ParameterException pe) {
+            log.error("Failed to parse command line parameters: " + Arrays.toString(args), pe);
+
+            jCommander.usage();
+
+            return;
+        }
+
+        String prop = cfg.configPath();
+
+        AgentConfiguration propCfg = new AgentConfiguration();
+
+        try {
+            File f = AgentUtils.resolvePath(prop);
+
+            if (f == null)
+                log.warn("Failed to find agent property file: {}", prop);
+            else
+                propCfg.load(f.toURI().toURL());
+        }
+        catch (IOException e) {
+            if (!AgentConfiguration.DFLT_CFG_PATH.equals(prop))
+                log.warn("Failed to load agent property file: " + prop, e);
+        }
+
+        cfg.merge(propCfg);
+
+        if (cfg.help()) {
+            jCommander.usage();
+
+            return;
+        }
+
+        System.out.println();
+        System.out.println("Agent configuration:");
+        System.out.println(cfg);
+        System.out.println();
+
+        URI uri;
+
+        try {
+            uri = new URI(cfg.serverUri());
+        }
+        catch (URISyntaxException e) {
+            log.error("Failed to parse Ignite Web Console uri", e);
+
+            return;
+        }
+
+        if (cfg.tokens() == null) {
+            System.out.println("Security token is required to establish connection to the web console.");
+            System.out.println(String.format("It is available on the Profile page: https://%s/profile", uri.getHost()));
+
+            String tokens = String.valueOf(readPassword("Enter security tokens separated by comma: "));
+
+            cfg.tokens(new ArrayList<>(Arrays.asList(tokens.trim().split(","))));
+        }
+
+        // Create proxy authenticator using passed properties.
+        switch (uri.getScheme()) {
+            case "http":
+            case "https":
+                final String username = System.getProperty(uri.getScheme() + ".proxyUsername");
+                final char[] pwd = System.getProperty(uri.getScheme() + ".proxyPassword", "").toCharArray();
+
+                Authenticator.setDefault(new Authenticator() {
+                    @Override protected PasswordAuthentication getPasswordAuthentication() {
+                        return new PasswordAuthentication(username, pwd);
+                    }
+                });
+
+                break;
+
+            default:
+                // No-op.
+        }
+
+        List<String> nodeURIs = cfg.nodeURIs();
+
+        for (int i = nodeURIs.size() - 1; i >= 0; i--) {
+            String nodeURI = nodeURIs.get(i);
+
+            try {
+                new URI(nodeURI);
+            }
+            catch (URISyntaxException ignored) {
+                log.warn("Failed to parse Ignite node URI: {}.", nodeURI);
+
+                nodeURIs.remove(i);
+            }
+        }
+
+        if (nodeURIs.isEmpty()) {
+            log.error("Failed to find valid URIs for connect to Ignite node via REST. Please check agent settings");
+
+            return;
+        }
+
+        boolean serverTrustAll = Boolean.getBoolean("trust.all");
+        boolean hasServerTrustStore = cfg.serverTrustStore() != null;
+
+        if (serverTrustAll && hasServerTrustStore) {
+            log.warn("Options contains both '--server-trust-store' and '-Dtrust.all=true'. " +
+                "Option '-Dtrust.all=true' will be ignored on connect to Web server.");
+
+            serverTrustAll = false;
+        }
+
+        boolean nodeTrustAll = Boolean.getBoolean("trust.all");
+        boolean hasNodeTrustStore = cfg.nodeTrustStore() != null;
+
+        if (nodeTrustAll && hasNodeTrustStore) {
+            log.warn("Options contains both '--node-trust-store' and '-Dtrust.all=true'. " +
+                "Option '-Dtrust.all=true' will be ignored on connect to cluster.");
+
+            nodeTrustAll = false;
+        }
+
+        cfg.nodeURIs(nodeURIs);
+
+        IO.Options opts = new IO.Options();
+        opts.path = "/agents";
+
+        List<String> cipherSuites = cfg.cipherSuites();
+
+        OkHttpClient.Builder builder = new OkHttpClient.Builder()
+            .proxyAuthenticator(new ProxyAuthenticator());
+
+        if (
+            serverTrustAll ||
+            hasServerTrustStore ||
+            cfg.serverKeyStore() != null
+        ) {
+            X509TrustManager serverTrustMgr = trustManager(
+                serverTrustAll,
+                cfg.serverTrustStore(),
+                cfg.serverTrustStorePassword()
+            );
+
+            if (serverTrustAll)
+                builder.hostnameVerifier((hostname, session) -> true);
+
+            SSLSocketFactory sslSocketFactory = sslSocketFactory(
+                cfg.serverKeyStore(),
+                cfg.serverKeyStorePassword(),
+                serverTrustMgr,
+                cipherSuites
+            );
+
+            if (sslSocketFactory != null) {
+                if (serverTrustMgr != null)
+                    builder.sslSocketFactory(sslSocketFactory, serverTrustMgr);
+                else
+                    builder.sslSocketFactory(sslSocketFactory);
+
+                if (!F.isEmpty(cipherSuites))
+                    builder.connectionSpecs(sslConnectionSpec(cipherSuites));
+            }
+
+            opts.secure = true;
+        }
+
+        OkHttpClient okHttpClient = builder.build();
+        
+        opts.callFactory = okHttpClient;
+        opts.webSocketFactory = okHttpClient;
+
+        final Socket client = IO.socket(uri, opts);
+
+        try (
+            RestExecutor restExecutor = new RestExecutor(
+                nodeTrustAll,
+                cfg.nodeKeyStore(), cfg.nodeKeyStorePassword(),
+                cfg.nodeTrustStore(), cfg.nodeTrustStorePassword(),
+                cipherSuites);
+
+            ClusterListener clusterLsnr = new ClusterListener(cfg, client, restExecutor)
+        ) {
+            Emitter.Listener onConnect = connectRes -> {
+                log.info("Connection established.");
+
+                JSONObject authMsg = new JSONObject();
+
+                try {
+                    authMsg.put("tokens", toJSON(cfg.tokens()));
+                    authMsg.put("disableDemo", cfg.disableDemo());
+
+                    String clsName = AgentLauncher.class.getSimpleName() + ".class";
+
+                    String clsPath = AgentLauncher.class.getResource(clsName).toString();
+
+                    if (clsPath.startsWith("jar")) {
+                        String manifestPath = clsPath.substring(0, clsPath.lastIndexOf('!') + 1) +
+                            "/META-INF/MANIFEST.MF";
+
+                        Manifest manifest = new Manifest(new URL(manifestPath).openStream());
+
+                        Attributes attr = manifest.getMainAttributes();
+
+                        authMsg.put("ver", attr.getValue("Implementation-Version"));
+                        authMsg.put("bt", attr.getValue("Build-Time"));
+                    }
+
+                    client.emit("agent:auth", authMsg, (Ack) authRes -> {
+                        if (authRes != null) {
+                            if (authRes[0] instanceof String) {
+                                log.error((String)authRes[0]);
+
+                                System.exit(1);
+                            }
+
+                            if (authRes[0] == null && authRes[1] instanceof JSONArray) {
+                                try {
+                                    List<String> activeTokens = fromJSON(authRes[1], List.class);
+
+                                    if (!F.isEmpty(activeTokens)) {
+                                        Collection<String> missedTokens = cfg.tokens();
+
+                                        cfg.tokens(activeTokens);
+
+                                        missedTokens.removeAll(activeTokens);
+
+                                        if (!F.isEmpty(missedTokens)) {
+                                            String tokens = F.concat(missedTokens, ", ");
+
+                                            log.warn("Failed to authenticate with token(s): {}. " +
+                                                "Please reload agent archive or check settings", tokens);
+                                        }
+
+                                        log.info("Authentication success.");
+
+                                        clusterLsnr.watch();
+
+                                        return;
+                                    }
+                                }
+                                catch (Exception e) {
+                                    log.error("Failed to authenticate agent. Please check agent\'s tokens", e);
+
+                                    System.exit(1);
+                                }
+                            }
+                        }
+
+                        log.error("Failed to authenticate agent. Please check agent\'s tokens");
+
+                        System.exit(1);
+                    });
+                }
+                catch (JSONException | IOException e) {
+                    log.error("Failed to construct authentication message", e);
+
+                    client.close();
+                }
+            };
+
+            DatabaseListener dbHnd = new DatabaseListener(cfg);
+
+            RestListener restHnd = new RestListener(cfg, restExecutor);
+
+            final CountDownLatch latch = new CountDownLatch(1);
+
+            log.info("Connecting to: {}", cfg.serverUri());
+
+            client
+                .on(EVENT_CONNECT, onConnect)
+                .on(EVENT_CONNECT_ERROR, onError)
+                .on(EVENT_ERROR, onError)
+                .on(EVENT_DISCONNECT, onDisconnect)
+                .on(EVENT_LOG_WARNING, onLogWarning)
+                .on(EVENT_RESET_TOKEN, res -> {
+                    String tok = String.valueOf(res[0]);
+
+                    log.warn("Security token has been reset: {}", tok);
+
+                    cfg.tokens().remove(tok);
+
+                    if (cfg.tokens().isEmpty()) {
+                        client.off();
+
+                        latch.countDown();
+                    }
+                })
+                .on(EVENT_SCHEMA_IMPORT_DRIVERS, dbHnd.availableDriversListener())
+                .on(EVENT_SCHEMA_IMPORT_SCHEMAS, dbHnd.schemasListener())
+                .on(EVENT_SCHEMA_IMPORT_METADATA, dbHnd.metadataListener())
+                .on(EVENT_NODE_VISOR_TASK, restHnd)
+                .on(EVENT_NODE_REST, restHnd);
+
+            client.connect();
+
+            latch.await();
+        }
+        finally {
+            client.close();
+        }
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/AgentUtils.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/AgentUtils.java
new file mode 100644
index 0000000..1b3e6a2
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/AgentUtils.java
@@ -0,0 +1,333 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.ProtectionDomain;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsonorg.JsonOrgModule;
+import io.socket.client.Ack;
+import okhttp3.ConnectionSpec;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.ssl.SSLContextWrapper;
+import org.apache.log4j.Logger;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+/**
+ * Utility methods.
+ */
+public class AgentUtils {
+    /** */
+    private static final Logger log = Logger.getLogger(AgentUtils.class.getName());
+
+    /** */
+    private static final char[] EMPTY_PWD = new char[0];
+
+    /** JSON object mapper. */
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    static {
+        // Register special module with basic serializers.
+        MAPPER.registerModule(new JsonOrgModule());
+    }
+
+    /** */
+    private static final Ack NOOP_CB = args -> {
+        if (args != null && args.length > 0 && args[0] instanceof Throwable)
+            log.error("Failed to execute request on agent.", (Throwable)args[0]);
+        else
+            log.info("Request on agent successfully executed " + Arrays.toString(args));
+    };
+
+    /**
+     * Default constructor.
+     */
+    private AgentUtils() {
+        // No-op.
+    }
+
+    /**
+     * @param path Path to normalize.
+     * @return Normalized file path.
+     */
+    public static String normalizePath(String path) {
+        return path != null ? path.replace('\\', '/') : null;
+    }
+
+    /**
+     * @return App folder.
+     */
+    public static File getAgentHome() {
+        try {
+            ProtectionDomain domain = AgentLauncher.class.getProtectionDomain();
+
+            // Should not happen, but to make sure our code is not broken.
+            if (domain == null || domain.getCodeSource() == null || domain.getCodeSource().getLocation() == null) {
+                log.warn("Failed to resolve agent jar location!");
+
+                return null;
+            }
+
+            // Resolve path to class-file.
+            URI classesUri = domain.getCodeSource().getLocation().toURI();
+
+            boolean win = System.getProperty("os.name").toLowerCase().contains("win");
+
+            // Overcome UNC path problem on Windows (http://www.tomergabel.com/JavaMishandlesUNCPathsOnWindows.aspx)
+            if (win && classesUri.getAuthority() != null)
+                classesUri = new URI(classesUri.toString().replace("file://", "file:/"));
+
+            return new File(classesUri).getParentFile();
+        }
+        catch (URISyntaxException | SecurityException ignored) {
+            log.warn("Failed to resolve agent jar location!");
+
+            return null;
+        }
+    }
+
+    /**
+     * Gets file associated with path.
+     * <p>
+     * First check if path is relative to agent home.
+     * If not, check if path is absolute.
+     * If all checks fail, then {@code null} is returned.
+     * <p>
+     *
+     * @param path Path to resolve.
+     * @return Resolved path as file, or {@code null} if path cannot be resolved.
+     */
+    public static File resolvePath(String path) {
+        assert path != null;
+
+        File home = getAgentHome();
+
+        if (home != null) {
+            File file = new File(home, normalizePath(path));
+
+            if (file.exists())
+                return file;
+        }
+
+        // 2. Check given path as absolute.
+        File file = new File(path);
+
+        if (file.exists())
+            return file;
+
+        return null;
+    }
+
+    /**
+     * Get callback from handler arguments.
+     *
+     * @param args Arguments.
+     * @return Callback or noop callback.
+     */
+    public static Ack safeCallback(Object[] args) {
+        boolean hasCb = args != null && args.length > 0 && args[args.length - 1] instanceof Ack;
+
+        return hasCb ? (Ack)args[args.length - 1] : NOOP_CB;
+    }
+
+    /**
+     * Remove callback from handler arguments.
+     *
+     * @param args Arguments.
+     * @return Arguments without callback.
+     */
+    public static Object[] removeCallback(Object[] args) {
+        boolean hasCb = args != null && args.length > 0 && args[args.length - 1] instanceof Ack;
+
+        return hasCb ? Arrays.copyOf(args, args.length - 1) : args;
+    }
+
+    /**
+     * Map java object to JSON object.
+     *
+     * @param obj Java object.
+     * @return {@link JSONObject} or {@link JSONArray}.
+     * @throws IllegalArgumentException If conversion fails due to incompatible type.
+     */
+    public static Object toJSON(Object obj) {
+        if (obj instanceof Iterable)
+            return MAPPER.convertValue(obj, JSONArray.class);
+
+        return MAPPER.convertValue(obj, JSONObject.class);
+    }
+
+    /**
+     * Map JSON object to java object.
+     *
+     * @param obj {@link JSONObject} or {@link JSONArray}.
+     * @param toValType Expected value type.
+     * @return Mapped object type of {@link T}.
+     * @throws IllegalArgumentException If conversion fails due to incompatible type.
+     */
+    public static <T> T fromJSON(Object obj, Class<T> toValType) throws IllegalArgumentException {
+        return MAPPER.convertValue(obj, toValType);
+    }
+
+    /**
+     * @param pathToJks Path to java key store file.
+     * @param pwd Key store password.
+     * @return Key store.
+     * @throws GeneralSecurityException If failed to load key store.
+     * @throws IOException If failed to load key store file content.
+     */
+    private static KeyStore keyStore(String pathToJks, char[] pwd) throws GeneralSecurityException, IOException {
+        KeyStore keyStore = KeyStore.getInstance("JKS");
+        keyStore.load(new FileInputStream(pathToJks), pwd);
+
+        return keyStore;
+    }
+
+    /**
+     * @param keyStorePath Path to key store.
+     * @param keyStorePwd Key store password.
+     * @return Key managers.
+     * @throws GeneralSecurityException If failed to load key store.
+     * @throws IOException If failed to load key store file content.
+     */
+    private static KeyManager[] keyManagers(String keyStorePath, String keyStorePwd)
+        throws GeneralSecurityException, IOException {
+        if (keyStorePath == null)
+            return null;
+
+        char[] keyPwd = keyStorePwd != null ? keyStorePwd.toCharArray() : EMPTY_PWD;
+
+        KeyStore keyStore = keyStore(keyStorePath, keyPwd);
+
+        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+        kmf.init(keyStore, keyPwd);
+
+        return kmf.getKeyManagers();
+    }
+
+    /**
+     * @param trustAll {@code true} If we trust to self-signed sertificates.
+     * @param trustStorePath Path to trust store file.
+     * @param trustStorePwd Trust store password.
+     * @return Trust manager
+     * @throws GeneralSecurityException If failed to load trust store.
+     * @throws IOException If failed to load trust store file content.
+     */
+    public static X509TrustManager trustManager(boolean trustAll, String trustStorePath, String trustStorePwd)
+        throws GeneralSecurityException, IOException {
+        if (trustAll)
+            return disabledTrustManager();
+
+        if (trustStorePath == null)
+            return null;
+
+        char[] trustPwd = trustStorePwd != null ? trustStorePwd.toCharArray() : EMPTY_PWD;
+        KeyStore trustKeyStore = keyStore(trustStorePath, trustPwd);
+
+        TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
+        tmf.init(trustKeyStore);
+
+        TrustManager[] trustMgrs = tmf.getTrustManagers();
+
+        return (X509TrustManager)Arrays.stream(trustMgrs)
+            .filter(tm -> tm instanceof X509TrustManager)
+            .findFirst()
+            .orElseThrow(() -> new IllegalStateException("X509TrustManager manager not found"));
+    }
+
+    /**
+     * Create SSL socket factory.
+     *
+     * @param keyStorePath Path to key store.
+     * @param keyStorePwd Key store password.
+     * @param trustMgr Trust manager.
+     * @param cipherSuites Optional cipher suites.
+     * @throws GeneralSecurityException If failed to load trust store.
+     * @throws IOException If failed to load store file content.
+     */
+    public static SSLSocketFactory sslSocketFactory(
+        String keyStorePath, String keyStorePwd,
+        X509TrustManager trustMgr,
+        List<String> cipherSuites
+    ) throws GeneralSecurityException, IOException {
+        KeyManager[] keyMgrs = keyManagers(keyStorePath, keyStorePwd);
+
+        if (keyMgrs == null && trustMgr == null)
+            return null;
+
+        SSLContext ctx = SSLContext.getInstance("TLS");
+
+        if (!F.isEmpty(cipherSuites))
+            ctx = new SSLContextWrapper(ctx, new SSLParameters(cipherSuites.toArray(new String[0])));
+
+        ctx.init(keyMgrs, new TrustManager[] {trustMgr}, null);
+
+        return ctx.getSocketFactory();
+    }
+
+    /**
+     * Create SSL configuration.
+     *
+     * @param cipherSuites SSL cipher suites.
+     */
+    public static List<ConnectionSpec> sslConnectionSpec(List<String> cipherSuites) {
+        return Collections.singletonList(new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+            .cipherSuites(cipherSuites.toArray(new String[0]))
+            .build());
+    }
+
+    /**
+     * Create a trust manager that trusts all certificates.
+     */
+    private static X509TrustManager disabledTrustManager() {
+        return new X509TrustManager() {
+            /** {@inheritDoc} */
+            @Override public X509Certificate[] getAcceptedIssuers() {
+                return new X509Certificate[0];
+            }
+
+            /** {@inheritDoc} */
+            @Override public void checkClientTrusted(X509Certificate[] certs, String authType) {
+                // No-op.
+            }
+
+            /** {@inheritDoc} */
+            @Override public void checkServerTrusted(X509Certificate[] certs, String authType) {
+                // No-op.
+            }
+        };
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/ProxyAuthException.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/ProxyAuthException.java
new file mode 100644
index 0000000..f293490
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/ProxyAuthException.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent;
+
+import java.io.IOException;
+
+/**
+ * This class extends {@link IOException} and represents an authentication failure to the proxy.
+ */
+public class ProxyAuthException extends IOException {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /**
+     * {@inheritDoc}
+     */
+    public ProxyAuthException(String msg) {
+        super(msg);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public ProxyAuthException(String msg, Throwable ex) {
+        super(msg, ex);
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/ProxyAuthenticator.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/ProxyAuthenticator.java
new file mode 100644
index 0000000..e0def22
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/ProxyAuthenticator.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.PasswordAuthentication;
+import java.net.Proxy;
+import java.util.List;
+import okhttp3.Authenticator;
+import okhttp3.Challenge;
+import okhttp3.Credentials;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.Route;
+
+import static java.net.Authenticator.RequestorType.PROXY;
+
+/**
+ * Request interactive proxy credentials.
+ *
+ * Configure OkHttp to use {@link OkHttpClient.Builder#proxyAuthenticator(Authenticator)}.
+ */
+public class ProxyAuthenticator implements Authenticator {
+    /** Latest credential hash code. */
+    private int latestCredHashCode = 0;
+
+    /** {@inheritDoc} */
+    @Override public Request authenticate(Route route, Response res) throws IOException {
+        List<Challenge> challenges = res.challenges();
+
+        for (Challenge challenge : challenges) {
+            if (!"Basic".equalsIgnoreCase(challenge.scheme()))
+                continue;
+
+            Request req = res.request();
+            HttpUrl url = req.url();
+            Proxy proxy = route.proxy();
+
+            InetSocketAddress proxyAddr = (InetSocketAddress)proxy.address();
+
+            PasswordAuthentication auth = java.net.Authenticator.requestPasswordAuthentication(
+                proxyAddr.getHostName(), proxyAddr.getAddress(), proxyAddr.getPort(),
+                url.scheme(), challenge.realm(), challenge.scheme(), url.url(), PROXY);
+
+            if (auth != null) {
+                String cred = Credentials.basic(auth.getUserName(), new String(auth.getPassword()), challenge.charset());
+
+                if (latestCredHashCode == cred.hashCode()) {
+                    latestCredHashCode = 0;
+
+                    throw new ProxyAuthException("Failed to authenticate with proxy");
+                }
+
+                latestCredHashCode = cred.hashCode();
+
+                return req.newBuilder()
+                    .header("Proxy-Authorization", cred)
+                    .build();
+            }
+        }
+
+        return null; // No challenges were satisfied!
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/DbColumn.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/DbColumn.java
new file mode 100644
index 0000000..61c15ba
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/DbColumn.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.db;
+
+import org.apache.ignite.internal.util.typedef.internal.S;
+
+/**
+ * Database table column.
+ */
+public class DbColumn {
+    /** Column name. */
+    private final String name;
+
+    /** Column JDBC type. */
+    private final int type;
+
+    /** Is this column belongs to primary key. */
+    private final boolean key;
+
+    /** Is {@code NULL} allowed for column in database. */
+    private final boolean nullable;
+
+    /** Whether column unsigned. */
+    private final boolean unsigned;
+
+    /**
+     * @param name Column name.
+     * @param type Column JDBC type.
+     * @param key {@code true} if this column belongs to primary key.
+     * @param nullable {@code true} if {@code NULL } allowed for column in database.
+     * @param unsigned {@code true} if column is unsigned.
+     */
+    public DbColumn(String name, int type, boolean key, boolean nullable, boolean unsigned) {
+        this.name = name;
+        this.type = type;
+        this.key = key;
+        this.nullable = nullable;
+        this.unsigned = unsigned;
+    }
+
+    /**
+     * @return Column name.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * @return Column JDBC type.
+     */
+    public int getType() {
+        return type;
+    }
+
+    /**
+     * @return {@code true} if this column belongs to primary key.
+     */
+    public boolean isKey() {
+        return key;
+    }
+
+    /**
+     * @return {@code true} if {@code NULL } allowed for column in database.
+     */
+    public boolean isNullable() {
+        return nullable;
+    }
+
+    /**
+     * @return {@code true} if column is unsigned.
+     */
+    public boolean isUnsigned() {
+        return unsigned;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(DbColumn.class, this);
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/DbMetadataReader.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/DbMetadataReader.java
new file mode 100644
index 0000000..95b54de
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/DbMetadataReader.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.db;
+
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.sql.Connection;
+import java.sql.Driver;
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import org.apache.ignite.console.agent.db.dialect.DB2MetadataDialect;
+import org.apache.ignite.console.agent.db.dialect.DatabaseMetadataDialect;
+import org.apache.ignite.console.agent.db.dialect.JdbcMetadataDialect;
+import org.apache.ignite.console.agent.db.dialect.MySQLMetadataDialect;
+import org.apache.ignite.console.agent.db.dialect.OracleMetadataDialect;
+import org.apache.log4j.Logger;
+
+/**
+ * Singleton to extract database metadata.
+ */
+public class DbMetadataReader {
+    /** Logger. */
+    private static final Logger log = Logger.getLogger(DbMetadataReader.class.getName());
+
+    /** */
+    private final Map<String, Driver> drivers = new HashMap<>();
+
+    /**
+     * Get specified dialect object for selected database.
+     *
+     * @param conn Connection to database.
+     * @return Specific dialect object.
+     */
+    private DatabaseMetadataDialect dialect(Connection conn) {
+        try {
+            String dbProductName = conn.getMetaData().getDatabaseProductName();
+
+            if ("Oracle".equals(dbProductName))
+                return new OracleMetadataDialect();
+
+            if (dbProductName.startsWith("DB2/"))
+                return new DB2MetadataDialect();
+
+            if ("MySQL".equals(dbProductName))
+                return new MySQLMetadataDialect();
+
+            return new JdbcMetadataDialect();
+        }
+        catch (SQLException e) {
+            log.error("Failed to resolve dialect (JdbcMetaDataDialect will be used.", e);
+
+            return new JdbcMetadataDialect();
+        }
+    }
+
+    /**
+     * Get list of schemas from database.
+     *
+     * @param conn Connection to database.
+     * @return List of schema names.
+     * @throws SQLException If schemas loading failed.
+     */
+    public Collection<String> schemas(Connection conn) throws SQLException {
+        return dialect(conn).schemas(conn);
+    }
+
+    /**
+     * Extract DB metadata.
+     *
+     * @param conn Connection.
+     * @param schemas List of database schemas to process. In case of empty list all schemas will be processed.
+     * @param tblsOnly Tables only flag.
+     */
+    public Collection<DbTable> metadata(Connection conn, List<String> schemas, boolean tblsOnly) throws SQLException {
+        return dialect(conn).tables(conn, schemas, tblsOnly);
+    }
+
+    /**
+     * Connect to database.
+     *
+     * @param jdbcDrvJarPath Path to JDBC driver.
+     * @param jdbcDrvCls JDBC class name.
+     * @param jdbcUrl JDBC connection URL.
+     * @param jdbcInfo Connection properties.
+     * @return Connection to database.
+     * @throws SQLException if connection failed.
+     */
+    public Connection connect(String jdbcDrvJarPath, String jdbcDrvCls, String jdbcUrl, Properties jdbcInfo)
+        throws SQLException {
+        Driver drv = drivers.get(jdbcDrvCls);
+
+        if (drv == null) {
+            if (jdbcDrvJarPath.isEmpty())
+                throw new IllegalStateException("Driver jar file name is not specified.");
+
+            File drvJar = new File(jdbcDrvJarPath);
+
+            if (!drvJar.exists())
+                throw new IllegalStateException("Driver jar file is not found.");
+
+            try {
+                URL u = new URL("jar:" + drvJar.toURI() + "!/");
+
+                URLClassLoader ucl = URLClassLoader.newInstance(new URL[] {u});
+
+                drv = (Driver)Class.forName(jdbcDrvCls, true, ucl).newInstance();
+
+                drivers.put(jdbcDrvCls, drv);
+            }
+            catch (Exception e) {
+                throw new IllegalStateException(e);
+            }
+        }
+
+        Connection conn = drv.connect(jdbcUrl, jdbcInfo);
+
+        if (conn == null)
+            throw new IllegalStateException("Connection was not established (JDBC driver returned null value).");
+
+        return conn;
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/DbSchema.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/DbSchema.java
new file mode 100644
index 0000000..1c89ceb
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/DbSchema.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.db;
+
+import java.util.Collection;
+import org.apache.ignite.internal.util.typedef.internal.S;
+
+/**
+ * Database schema names with catalog name.
+ */
+public class DbSchema {
+    /** Catalog name. */
+    private final String catalog;
+
+    /** Schema names. */
+    private final Collection<String> schemas;
+
+    /**
+     * @param catalog Catalog name.
+     * @param schemas Schema names.
+     */
+    public DbSchema(String catalog, Collection<String> schemas) {
+        this.catalog = catalog;
+        this.schemas = schemas;
+    }
+
+    /**
+     * @return Catalog name.
+     */
+    public String getCatalog() {
+        return catalog;
+    }
+
+    /**
+     * @return Schema names.
+     */
+    public Collection<String> getSchemas() {
+        return schemas;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(DbSchema.class, this);
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/DbTable.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/DbTable.java
new file mode 100644
index 0000000..653bb07
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/DbTable.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.db;
+
+import java.util.Collection;
+import org.apache.ignite.internal.util.typedef.internal.S;
+import org.apache.ignite.internal.visor.query.VisorQueryIndex;
+
+/**
+ * Database table.
+ */
+public class DbTable {
+    /** Schema name. */
+    private final String schema;
+
+    /** Table name. */
+    private final String tbl;
+
+    /** Columns. */
+    private final Collection<DbColumn> cols;
+
+    /** Indexes. */
+    private final Collection<VisorQueryIndex> idxs;
+
+    /**
+     * Default columns.
+     *
+     * @param schema Schema name.
+     * @param tbl Table name.
+     * @param cols Columns.
+     * @param idxs Indexes;
+     */
+    public DbTable(String schema, String tbl, Collection<DbColumn> cols, Collection<VisorQueryIndex> idxs) {
+        this.schema = schema;
+        this.tbl = tbl;
+        this.cols = cols;
+        this.idxs = idxs;
+    }
+
+    /**
+     * @return Schema name.
+     */
+    public String getSchema() {
+        return schema;
+    }
+
+    /**
+     * @return Table name.
+     */
+    public String getTable() {
+        return tbl;
+    }
+
+    /**
+     * @return Columns.
+     */
+    public Collection<DbColumn> getColumns() {
+        return cols;
+    }
+
+    /**
+     * @return Indexes.
+     */
+    public Collection<VisorQueryIndex> getIndexes() {
+        return idxs;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(DbTable.class, this);
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/DB2MetadataDialect.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/DB2MetadataDialect.java
new file mode 100644
index 0000000..2f41a8c
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/DB2MetadataDialect.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.db.dialect;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * DB2 specific metadata dialect.
+ */
+public class DB2MetadataDialect extends JdbcMetadataDialect {
+    /** {@inheritDoc} */
+    @Override public Set<String> systemSchemas() {
+        return new HashSet<>(Arrays.asList("SYSIBM", "SYSCAT", "SYSSTAT", "SYSTOOLS", "SYSFUN", "SYSIBMADM",
+            "SYSIBMINTERNAL", "SYSIBMTS", "SYSPROC", "SYSPUBLIC"));
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/DatabaseMetadataDialect.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/DatabaseMetadataDialect.java
new file mode 100644
index 0000000..13faea0
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/DatabaseMetadataDialect.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.db.dialect;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.cache.QueryIndex;
+import org.apache.ignite.cache.QueryIndexType;
+import org.apache.ignite.console.agent.db.DbColumn;
+import org.apache.ignite.console.agent.db.DbTable;
+import org.apache.ignite.internal.visor.query.VisorQueryIndex;
+
+/**
+ * Base class for database metadata dialect.
+ */
+public abstract class DatabaseMetadataDialect {
+    /**
+     * Gets schemas from database.
+     *
+     * @param conn Database connection.
+     * @return Collection of schema descriptors.
+     * @throws SQLException If failed to get schemas.
+     */
+    public abstract Collection<String> schemas(Connection conn) throws SQLException;
+
+    /**
+     * Gets tables from database.
+     *
+     * @param conn Database connection.
+     * @param schemas Collection of schema names to load.
+     * @param tblsOnly If {@code true} then gets only tables otherwise gets tables and views.
+     * @return Collection of table descriptors.
+     * @throws SQLException If failed to get tables.
+     */
+    public abstract Collection<DbTable> tables(Connection conn, List<String> schemas, boolean tblsOnly)
+        throws SQLException;
+
+    /**
+     * @return Collection of database system schemas.
+     */
+    public Set<String> systemSchemas() {
+        return Collections.singleton("INFORMATION_SCHEMA");
+    }
+
+    /**
+     * @return Collection of unsigned type names.
+     * @throws SQLException If failed to get unsigned type names.
+     */
+    public Set<String> unsignedTypes(DatabaseMetaData dbMeta) throws SQLException {
+        return Collections.emptySet();
+    }
+
+    /**
+     * Create table descriptor.
+     *
+     * @param schema Schema name.
+     * @param tbl Table name.
+     * @param cols Table columns.
+     * @param idxs Table indexes.
+     * @return New {@code DbTable} instance.
+     */
+    protected DbTable table(String schema, String tbl, Collection<DbColumn> cols, Collection<QueryIndex> idxs) {
+        Collection<VisorQueryIndex> res = new ArrayList<>(idxs.size());
+
+        for (QueryIndex idx : idxs)
+            res.add(new VisorQueryIndex(idx));
+
+        return new DbTable(schema, tbl, cols, res);
+    }
+
+    /**
+     * Create index descriptor.
+     *
+     * @param idxName Index name.
+     * @return New initialized {@code QueryIndex} instance.
+     */
+    protected QueryIndex index(String idxName) {
+        QueryIndex idx = new QueryIndex();
+
+        idx.setName(idxName);
+        idx.setIndexType(QueryIndexType.SORTED);
+        idx.setFields(new LinkedHashMap<String, Boolean>());
+
+        return idx;
+    }
+
+    /**
+     * Select first shortest index.
+     *
+     * @param uniqueIdxs Unique indexes with columns.
+     * @return Unique index that could be used instead of primary key.
+     */
+    protected Map.Entry<String, Set<String>> uniqueIndexAsPk(Map<String, Set<String>> uniqueIdxs) {
+        Map.Entry<String, Set<String>> uniqueIdxAsPk = null;
+
+        for (Map.Entry<String, Set<String>> uniqueIdx : uniqueIdxs.entrySet()) {
+            if (uniqueIdxAsPk == null || uniqueIdxAsPk.getValue().size() > uniqueIdx.getValue().size())
+                uniqueIdxAsPk = uniqueIdx;
+        }
+
+        return uniqueIdxAsPk;
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/JdbcMetadataDialect.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/JdbcMetadataDialect.java
new file mode 100644
index 0000000..c8cb24d
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/JdbcMetadataDialect.java
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.db.dialect;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.cache.QueryIndex;
+import org.apache.ignite.console.agent.db.DbColumn;
+import org.apache.ignite.console.agent.db.DbTable;
+
+/**
+ * Metadata dialect that uses standard JDBC for reading metadata.
+ */
+public class JdbcMetadataDialect extends DatabaseMetadataDialect {
+    /** */
+    private static final String[] TABLES_ONLY = {"TABLE"};
+
+    /** */
+    private static final String[] TABLES_AND_VIEWS = {"TABLE", "VIEW"};
+
+    /** Schema catalog index. */
+    private static final int TBL_CATALOG_IDX = 1;
+
+    /** Schema name index. */
+    private static final int TBL_SCHEMA_IDX = 2;
+
+    /** Table name index. */
+    private static final int TBL_NAME_IDX = 3;
+
+    /** Primary key column name index. */
+    private static final int PK_COL_NAME_IDX = 4;
+
+    /** Column name index. */
+    private static final int COL_NAME_IDX = 4;
+
+    /** Column data type index. */
+    private static final int COL_DATA_TYPE_IDX = 5;
+
+    /** Column type name index. */
+    private static final int COL_TYPE_NAME_IDX = 6;
+
+    /** Column nullable index. */
+    private static final int COL_NULLABLE_IDX = 11;
+
+    /** Index name index. */
+    private static final int IDX_NAME_IDX = 6;
+
+    /** Index column name index. */
+    private static final int IDX_COL_NAME_IDX = 9;
+
+    /** Index column descend index. */
+    private static final int IDX_ASC_OR_DESC_IDX = 10;
+
+    /** {@inheritDoc} */
+    @Override public Collection<String> schemas(Connection conn) throws SQLException {
+        Collection<String> schemas = new ArrayList<>();
+
+        ResultSet rs = conn.getMetaData().getSchemas();
+
+        Set<String> sys = systemSchemas();
+
+        while (rs.next()) {
+            String schema = rs.getString(1);
+
+            // Skip system schemas.
+            if (sys.contains(schema))
+                continue;
+
+            schemas.add(schema);
+        }
+
+        return schemas;
+    }
+
+    /**
+     * @return If {@code true} use catalogs for table division.
+     */
+    protected boolean useCatalog() {
+        return false;
+    }
+
+    /**
+     * @return If {@code true} use schemas for table division.
+     */
+    protected boolean useSchema() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public Collection<DbTable> tables(Connection conn, List<String> schemas, boolean tblsOnly)
+        throws SQLException {
+        DatabaseMetaData dbMeta = conn.getMetaData();
+
+        Set<String> sys = systemSchemas();
+
+        Collection<String> unsignedTypes = unsignedTypes(dbMeta);
+
+        if (schemas.isEmpty())
+            schemas.add(null);
+
+        Collection<DbTable> tbls = new ArrayList<>();
+
+        for (String toSchema: schemas) {
+            try (ResultSet tblsRs = dbMeta.getTables(useCatalog() ? toSchema : null, useSchema() ? toSchema : null, "%",
+                    tblsOnly ? TABLES_ONLY : TABLES_AND_VIEWS)) {
+                while (tblsRs.next()) {
+                    String tblCatalog = tblsRs.getString(TBL_CATALOG_IDX);
+                    String tblSchema = tblsRs.getString(TBL_SCHEMA_IDX);
+                    String tblName = tblsRs.getString(TBL_NAME_IDX);
+
+                    // In case of MySql we should use catalog.
+                    String schema = tblSchema != null ? tblSchema : tblCatalog;
+
+                    // Skip system schemas.
+                    if (sys.contains(schema))
+                        continue;
+
+                    Set<String> pkCols = new LinkedHashSet<>();
+
+                    try (ResultSet pkRs = dbMeta.getPrimaryKeys(tblCatalog, tblSchema, tblName)) {
+                        while (pkRs.next())
+                            pkCols.add(pkRs.getString(PK_COL_NAME_IDX));
+                    }
+
+                    Map.Entry<String, Set<String>> uniqueIdxAsPk = null;
+
+                    // If PK not found, trying to use first UNIQUE index as key.
+                    if (pkCols.isEmpty()) {
+                        Map<String, Set<String>> uniqueIdxs = new LinkedHashMap<>();
+
+                        try (ResultSet idxRs = dbMeta.getIndexInfo(tblCatalog, tblSchema, tblName, true, true)) {
+                            while (idxRs.next()) {
+                                String idxName = idxRs.getString(IDX_NAME_IDX);
+                                String colName = idxRs.getString(IDX_COL_NAME_IDX);
+
+                                if (idxName == null || colName == null)
+                                    continue;
+
+                                Set<String> idxCols = uniqueIdxs.get(idxName);
+
+                                if (idxCols == null) {
+                                    idxCols = new LinkedHashSet<>();
+
+                                    uniqueIdxs.put(idxName, idxCols);
+                                }
+
+                                idxCols.add(colName);
+                            }
+                        }
+
+                        uniqueIdxAsPk = uniqueIndexAsPk(uniqueIdxs);
+
+                        if (uniqueIdxAsPk != null)
+                            pkCols.addAll(uniqueIdxAsPk.getValue());
+                    }
+
+                    Collection<DbColumn> cols = new ArrayList<>();
+
+                    try (ResultSet colsRs = dbMeta.getColumns(tblCatalog, tblSchema, tblName, null)) {
+                        while (colsRs.next()) {
+                            String colName = colsRs.getString(COL_NAME_IDX);
+
+                            cols.add(new DbColumn(
+                                colName,
+                                colsRs.getInt(COL_DATA_TYPE_IDX),
+                                pkCols.contains(colName),
+                                colsRs.getInt(COL_NULLABLE_IDX) == DatabaseMetaData.columnNullable,
+                                unsignedTypes.contains(colsRs.getString(COL_TYPE_NAME_IDX))));
+                        }
+                    }
+
+                    String uniqueIdxAsPkName = uniqueIdxAsPk != null ? uniqueIdxAsPk.getKey() : null;
+
+                    Map<String, QueryIndex> idxs = new LinkedHashMap<>();
+
+                    try (ResultSet idxRs = dbMeta.getIndexInfo(tblCatalog, tblSchema, tblName, false, true)) {
+                        while (idxRs.next()) {
+                            String idxName = idxRs.getString(IDX_NAME_IDX);
+                            String colName = idxRs.getString(IDX_COL_NAME_IDX);
+
+                            // Skip {@code null} names and unique index used as PK.
+                            if (idxName == null || colName == null || idxName.equals(uniqueIdxAsPkName))
+                                continue;
+
+                            QueryIndex idx = idxs.get(idxName);
+
+                            if (idx == null) {
+                                idx = index(idxName);
+
+                                idxs.put(idxName, idx);
+                            }
+
+                            String askOrDesc = idxRs.getString(IDX_ASC_OR_DESC_IDX);
+
+                            Boolean asc = askOrDesc == null || "A".equals(askOrDesc);
+
+                            idx.getFields().put(colName, asc);
+                        }
+                    }
+
+                    // Remove index that is equals to primary key.
+                    if (!pkCols.isEmpty()) {
+                        for (Map.Entry<String, QueryIndex> entry : idxs.entrySet()) {
+                            QueryIndex idx = entry.getValue();
+
+                            if (pkCols.equals(idx.getFields().keySet())) {
+                                idxs.remove(entry.getKey());
+
+                                break;
+                            }
+                        }
+                    }
+
+                    tbls.add(table(schema, tblName, cols, idxs.values()));
+                }
+            }
+        }
+
+        return tbls;
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/MySQLMetadataDialect.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/MySQLMetadataDialect.java
new file mode 100644
index 0000000..f53a5e1
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/MySQLMetadataDialect.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.db.dialect;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * MySQL specific metadata dialect.
+ */
+public class MySQLMetadataDialect extends JdbcMetadataDialect {
+    /** Type name index. */
+    private static final int TYPE_NAME_IDX = 1;
+
+    /** {@inheritDoc} */
+    @Override public Set<String> systemSchemas() {
+        return new HashSet<>(Arrays.asList("information_schema", "mysql", "performance_schema", "sys"));
+    }
+
+    /** {@inheritDoc} */
+    @Override public Collection<String> schemas(Connection conn) throws SQLException {
+        Collection<String> schemas = new ArrayList<>();
+
+        ResultSet rs = conn.getMetaData().getCatalogs();
+
+        Set<String> sys = systemSchemas();
+
+        while (rs.next()) {
+            String schema = rs.getString(1);
+
+            // Skip system schemas.
+            if (sys.contains(schema))
+                continue;
+
+            schemas.add(schema);
+        }
+
+        return schemas;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected boolean useCatalog() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected boolean useSchema() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public Set<String> unsignedTypes(DatabaseMetaData dbMeta) throws SQLException {
+        Set<String> unsignedTypes = new HashSet<>();
+
+        try (ResultSet typeRs = dbMeta.getTypeInfo()) {
+            while (typeRs.next()) {
+                String typeName = typeRs.getString(TYPE_NAME_IDX);
+
+                if (typeName.contains("UNSIGNED"))
+                    unsignedTypes.add(typeName);
+            }
+        }
+
+        return unsignedTypes;
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/OracleMetadataDialect.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/OracleMetadataDialect.java
new file mode 100644
index 0000000..609510a
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/db/dialect/OracleMetadataDialect.java
@@ -0,0 +1,424 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.db.dialect;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.cache.QueryIndex;
+import org.apache.ignite.console.agent.db.DbColumn;
+import org.apache.ignite.console.agent.db.DbTable;
+
+import static java.sql.Types.BIGINT;
+import static java.sql.Types.BLOB;
+import static java.sql.Types.CHAR;
+import static java.sql.Types.CLOB;
+import static java.sql.Types.DATE;
+import static java.sql.Types.DOUBLE;
+import static java.sql.Types.FLOAT;
+import static java.sql.Types.INTEGER;
+import static java.sql.Types.LONGVARBINARY;
+import static java.sql.Types.LONGVARCHAR;
+import static java.sql.Types.NUMERIC;
+import static java.sql.Types.OTHER;
+import static java.sql.Types.SMALLINT;
+import static java.sql.Types.SQLXML;
+import static java.sql.Types.TIMESTAMP;
+import static java.sql.Types.TINYINT;
+import static java.sql.Types.VARCHAR;
+
+/**
+ * Oracle specific metadata dialect.
+ */
+public class OracleMetadataDialect extends DatabaseMetadataDialect {
+    /** SQL to get columns metadata. */
+    private static final String SQL_COLUMNS = "SELECT a.owner, a.table_name, a.column_name, a.nullable," +
+        " a.data_type, a.data_precision, a.data_scale" +
+        " FROM all_tab_columns a %s" +
+        " %s " +
+        " ORDER BY a.owner, a.table_name, a.column_id";
+
+    /** SQL to get list of PRIMARY KEYS columns. */
+    private static final String SQL_PRIMARY_KEYS = "SELECT b.column_name" +
+        " FROM all_constraints a" +
+        "  INNER JOIN all_cons_columns b" +
+        "   ON a.owner = b.owner" +
+        "  AND a.constraint_name = b.constraint_name" +
+        " WHERE a.owner = ? and a.table_name = ? AND a.constraint_type = 'P'";
+
+    /** SQL to get list of UNIQUE INDEX columns. */
+    private static final String SQL_UNIQUE_INDEXES_KEYS = "SELECT a.index_name, b.column_name" +
+        " FROM all_indexes a" +
+        " INNER JOIN all_ind_columns b" +
+        "   ON a.index_name = b.index_name" +
+        "  AND a.table_owner = b.table_owner" +
+        "  AND a.table_name = b.table_name" +
+        "  AND a.owner = b.index_owner" +
+        " WHERE a.owner = ? AND a.table_name = ? AND a.uniqueness = 'UNIQUE'" +
+        " ORDER BY b.column_position";
+
+    /** SQL to get indexes metadata. */
+    private static final String SQL_INDEXES = "SELECT i.index_name, u.column_expression, i.column_name, i.descend" +
+        " FROM all_ind_columns i" +
+        " LEFT JOIN user_ind_expressions u" +
+        "   ON u.index_name = i.index_name" +
+        "  AND i.table_name = u.table_name" +
+        " WHERE i.index_owner = ? and i.table_name = ?" +
+        " ORDER BY i.index_name, i.column_position";
+
+    /** Owner index. */
+    private static final int OWNER_IDX = 1;
+
+    /** Table name index. */
+    private static final int TBL_NAME_IDX = 2;
+
+    /** Column name index. */
+    private static final int COL_NAME_IDX = 3;
+
+    /** Nullable index. */
+    private static final int NULLABLE_IDX = 4;
+
+    /** Data type index. */
+    private static final int DATA_TYPE_IDX = 5;
+
+    /** Numeric precision index. */
+    private static final int DATA_PRECISION_IDX = 6;
+
+    /** Numeric scale index. */
+    private static final int DATA_SCALE_IDX = 7;
+
+    /** Unique index name index. */
+    private static final int UNQ_IDX_NAME_IDX = 1;
+
+    /** Unique index column name index. */
+    private static final int UNQ_IDX_COL_NAME_IDX = 2;
+
+    /** Index name index. */
+    private static final int IDX_NAME_IDX = 1;
+
+    /** Index name index. */
+    private static final int IDX_EXPR_IDX = 2;
+
+    /** Index column name index. */
+    private static final int IDX_COL_NAME_IDX = 3;
+
+    /** Index column sort order index. */
+    private static final int IDX_COL_DESCEND_IDX = 4;
+
+    /** {@inheritDoc} */
+    @Override public Set<String> systemSchemas() {
+        return new HashSet<>(Arrays.asList("ANONYMOUS", "APPQOSSYS", "CTXSYS", "DBSNMP", "EXFSYS", "LBACSYS", "MDSYS",
+            "MGMT_VIEW", "OLAPSYS", "OWBSYS", "ORDPLUGINS", "ORDSYS", "OUTLN", "SI_INFORMTN_SCHEMA", "SYS", "SYSMAN",
+            "SYSTEM", "TSMSYS", "WK_TEST", "WKSYS", "WKPROXY", "WMSYS", "XDB",
+
+            "APEX_040000", "APEX_PUBLIC_USER", "DIP", "FLOWS_30000", "FLOWS_FILES", "MDDATA", "ORACLE_OCM",
+            "SPATIAL_CSW_ADMIN_USR", "SPATIAL_WFS_ADMIN_USR", "XS$NULL",
+
+            "BI", "HR", "OE", "PM", "IX", "SH"));
+    }
+
+    /** {@inheritDoc} */
+    @Override public Collection<String> schemas(Connection conn) throws SQLException {
+        Collection<String> schemas = new ArrayList<>();
+
+        ResultSet rs = conn.getMetaData().getSchemas();
+
+        Set<String> sysSchemas = systemSchemas();
+
+        while (rs.next()) {
+            String schema = rs.getString(1);
+
+            if (!sysSchemas.contains(schema) && !schema.startsWith("FLOWS_"))
+                schemas.add(schema);
+        }
+
+        return schemas;
+    }
+
+    /**
+     * @param rs Result set with column type metadata from Oracle database.
+     * @return JDBC type.
+     * @throws SQLException If failed to decode type.
+     */
+    private int decodeType(ResultSet rs) throws SQLException {
+        String type = rs.getString(DATA_TYPE_IDX);
+
+        if (type.startsWith("TIMESTAMP"))
+            return TIMESTAMP;
+        else {
+            switch (type) {
+                case "CHAR":
+                case "NCHAR":
+                    return CHAR;
+
+                case "VARCHAR2":
+                case "NVARCHAR2":
+                    return VARCHAR;
+
+                case "LONG":
+                    return LONGVARCHAR;
+
+                case "LONG RAW":
+                    return LONGVARBINARY;
+
+                case "FLOAT":
+                    return FLOAT;
+
+                case "NUMBER":
+                    int precision = rs.getInt(DATA_PRECISION_IDX);
+                    int scale = rs.getInt(DATA_SCALE_IDX);
+
+                    if (scale > 0) {
+                        if (scale < 4 && precision < 19)
+                            return FLOAT;
+
+                        if (scale > 4 || precision > 19)
+                            return DOUBLE;
+
+                        return NUMERIC;
+                    }
+                    else {
+                        if (precision < 1)
+                            return NUMERIC;
+
+                        if (precision < 3)
+                            return TINYINT;
+
+                        if (precision < 5)
+                            return SMALLINT;
+
+                        if (precision < 10)
+                            return INTEGER;
+
+                        if (precision < 19)
+                            return BIGINT;
+
+                        return NUMERIC;
+                    }
+
+                case "DATE":
+                    return DATE;
+
+                case "BFILE":
+                case "BLOB":
+                    return BLOB;
+
+                case "CLOB":
+                case "NCLOB":
+                    return CLOB;
+
+                case "XMLTYPE":
+                    return SQLXML;
+            }
+        }
+
+        return OTHER;
+    }
+
+    /**
+     * Retrieve primary key columns.
+     *
+     * @param stmt Prepared SQL statement to execute.
+     * @param owner DB owner.
+     * @param tbl Table name.
+     * @return Primary key columns.
+     * @throws SQLException If failed to retrieve primary key columns.
+     */
+    private Set<String> primaryKeys(PreparedStatement stmt, String owner, String tbl) throws SQLException {
+        stmt.setString(1, owner);
+        stmt.setString(2, tbl);
+
+        Set<String> pkCols = new LinkedHashSet<>();
+
+        try (ResultSet pkRs = stmt.executeQuery()) {
+            while (pkRs.next())
+                pkCols.add(pkRs.getString(1));
+        }
+
+        return pkCols;
+    }
+
+    /**
+     * Retrieve unique indexes with columns.
+     *
+     * @param stmt Prepared SQL statement to execute.
+     * @param owner DB owner.
+     * @param tbl Table name.
+     * @return Unique indexes.
+     * @throws SQLException If failed to retrieve unique indexes columns.
+     */
+    private Map<String, Set<String>> uniqueIndexes(PreparedStatement stmt, String owner, String tbl) throws SQLException {
+        stmt.setString(1, owner);
+        stmt.setString(2, tbl);
+
+        Map<String, Set<String>> uniqueIdxs = new LinkedHashMap<>();
+
+        try (ResultSet idxsRs = stmt.executeQuery()) {
+            while (idxsRs.next()) {
+                String idxName = idxsRs.getString(UNQ_IDX_NAME_IDX);
+                String colName = idxsRs.getString(UNQ_IDX_COL_NAME_IDX);
+
+                Set<String> idxCols = uniqueIdxs.get(idxName);
+
+                if (idxCols == null) {
+                    idxCols = new LinkedHashSet<>();
+
+                    uniqueIdxs.put(idxName, idxCols);
+                }
+
+                idxCols.add(colName);
+            }
+        }
+
+        return uniqueIdxs;
+    }
+
+    /**
+     * Retrieve index columns.
+     *
+     * @param stmt Prepared SQL statement to execute.
+     * @param owner DB owner.
+     * @param tbl Table name.
+     * @param uniqueIdxAsPk Optional unique index that used as PK.
+     * @return Indexes.
+     * @throws SQLException If failed to retrieve indexes columns.
+     */
+    private Collection<QueryIndex> indexes(PreparedStatement stmt, String owner, String tbl, String uniqueIdxAsPk) throws SQLException {
+        stmt.setString(1, owner);
+        stmt.setString(2, tbl);
+
+        Map<String, QueryIndex> idxs = new LinkedHashMap<>();
+
+        try (ResultSet idxsRs = stmt.executeQuery()) {
+            while (idxsRs.next()) {
+                String idxName = idxsRs.getString(IDX_NAME_IDX);
+
+                // Skip unique index used as PK.
+                if (idxName.equals(uniqueIdxAsPk))
+                    continue;
+
+                QueryIndex idx = idxs.get(idxName);
+
+                if (idx == null) {
+                    idx = index(idxName);
+
+                    idxs.put(idxName, idx);
+                }
+
+                String expr = idxsRs.getString(IDX_EXPR_IDX);
+
+                String col = expr == null ? idxsRs.getString(IDX_COL_NAME_IDX) : expr.replaceAll("\"", "");
+
+                idx.getFields().put(col, !"DESC".equals(idxsRs.getString(IDX_COL_DESCEND_IDX)));
+            }
+        }
+
+        return idxs.values();
+    }
+
+    /** {@inheritDoc} */
+    @Override public Collection<DbTable> tables(Connection conn, List<String> schemas, boolean tblsOnly) throws SQLException {
+        PreparedStatement pkStmt = conn.prepareStatement(SQL_PRIMARY_KEYS);
+        PreparedStatement uniqueIdxsStmt = conn.prepareStatement(SQL_UNIQUE_INDEXES_KEYS);
+        PreparedStatement idxStmt = conn.prepareStatement(SQL_INDEXES);
+
+        if (schemas.isEmpty())
+            schemas.add(null);
+
+        Set<String> sysSchemas = systemSchemas();
+
+        Collection<DbTable> tbls = new ArrayList<>();
+
+        try (Statement colsStmt = conn.createStatement()) {
+            for (String schema: schemas) {
+                if (systemSchemas().contains(schema) || (schema != null && schema.startsWith("FLOWS_")))
+                    continue;
+
+                String sql = String.format(SQL_COLUMNS,
+                        tblsOnly ? "INNER JOIN all_tables b on a.table_name = b.table_name and a.owner = b.owner" : "",
+                        schema != null ? String.format(" WHERE a.owner = '%s' ", schema) : "");
+
+                try (ResultSet colsRs = colsStmt.executeQuery(sql)) {
+                    String prevSchema = "";
+                    String prevTbl = "";
+
+                    boolean first = true;
+
+                    Set<String> pkCols = Collections.emptySet();
+                    Collection<DbColumn> cols = new ArrayList<>();
+                    Collection<QueryIndex> idxs = Collections.emptyList();
+
+                    while (colsRs.next()) {
+                        String owner = colsRs.getString(OWNER_IDX);
+                        String tbl = colsRs.getString(TBL_NAME_IDX);
+
+                        if (sysSchemas.contains(owner) || (schema != null && schema.startsWith("FLOWS_")))
+                            continue;
+
+                        boolean changed = !owner.equals(prevSchema) || !tbl.equals(prevTbl);
+
+                        if (changed) {
+                            if (first)
+                                first = false;
+                            else
+                                tbls.add(table(prevSchema, prevTbl, cols, idxs));
+
+                            prevSchema = owner;
+                            prevTbl = tbl;
+                            cols = new ArrayList<>();
+                            pkCols = primaryKeys(pkStmt, owner, tbl);
+
+                            Map.Entry<String, Set<String>> uniqueIdxAsPk = null;
+
+                            if (pkCols.isEmpty()) {
+                                uniqueIdxAsPk = uniqueIndexAsPk(uniqueIndexes(uniqueIdxsStmt, owner, tbl));
+
+                                if (uniqueIdxAsPk != null)
+                                    pkCols.addAll(uniqueIdxAsPk.getValue());
+                            }
+
+                            idxs = indexes(idxStmt, owner, tbl, uniqueIdxAsPk != null ? uniqueIdxAsPk.getKey() : null);
+                        }
+
+                        String colName = colsRs.getString(COL_NAME_IDX);
+
+                        cols.add(new DbColumn(colName, decodeType(colsRs), pkCols.contains(colName),
+                            !"N".equals(colsRs.getString(NULLABLE_IDX)), false));
+                    }
+
+                    if (!cols.isEmpty())
+                        tbls.add(table(prevSchema, prevTbl, cols, idxs));
+                }
+            }
+        }
+
+        return tbls;
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/AbstractListener.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/AbstractListener.java
new file mode 100644
index 0000000..d38bf64
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/AbstractListener.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.handlers;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.zip.GZIPOutputStream;
+import io.socket.client.Ack;
+import io.socket.emitter.Emitter;
+import org.apache.commons.codec.binary.Base64OutputStream;
+import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.console.agent.rest.RestResult;
+import org.apache.ignite.logger.slf4j.Slf4jLogger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.ignite.console.agent.AgentUtils.fromJSON;
+import static org.apache.ignite.console.agent.AgentUtils.removeCallback;
+import static org.apache.ignite.console.agent.AgentUtils.safeCallback;
+import static org.apache.ignite.console.agent.AgentUtils.toJSON;
+
+/**
+ * Base class for web socket handlers.
+ */
+abstract class AbstractListener implements Emitter.Listener {
+    /** */
+    final IgniteLogger log = new Slf4jLogger(LoggerFactory.getLogger(AbstractListener.class));
+
+    /** UTF8 charset. */
+    private static final Charset UTF8 = Charset.forName("UTF-8");
+
+    /** */
+    private ExecutorService pool;
+
+    /** {@inheritDoc} */
+    @SuppressWarnings("unchecked")
+    @Override public final void call(Object... args) {
+        final Ack cb = safeCallback(args);
+
+        args = removeCallback(args);
+
+        try {
+            final Map<String, Object> params;
+
+            if (args == null || args.length == 0)
+                params = Collections.emptyMap();
+            else if (args.length == 1)
+                params = fromJSON(args[0], Map.class);
+            else
+                throw new IllegalArgumentException("Wrong arguments count, must be <= 1: " + Arrays.toString(args));
+
+            if (pool == null)
+                pool = newThreadPool();
+
+            pool.submit(() -> {
+                try {
+                    Object res = execute(params);
+
+                    // TODO IGNITE-6127 Temporary solution until GZip support for socket.io-client-java.
+                    // See: https://github.com/socketio/socket.io-client-java/issues/312
+                    // We can GZip manually for now.
+                    if (res instanceof RestResult) {
+                        RestResult restRes = (RestResult) res;
+
+                        if (restRes.getData() != null) {
+                            ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
+                            Base64OutputStream b64os = new Base64OutputStream(baos, true, 0, null);
+                            GZIPOutputStream gzip = new GZIPOutputStream(b64os);
+
+                            gzip.write(restRes.getData().getBytes(UTF8));
+
+                            gzip.close();
+
+                            restRes.zipData(baos.toString());
+                        }
+                    }
+
+                    cb.call(null, toJSON(res));
+                }
+                catch (Throwable e) {
+                    log.error("Failed to process event in pool", e);
+
+                    cb.call(e, null);
+                }
+            });
+        }
+        catch (Throwable e) {
+            log.error("Failed to process event", e);
+
+            cb.call(e, null);
+        }
+    }
+
+    /**
+     * Stop handler.
+     */
+    public void stop() {
+        if (pool != null)
+            pool.shutdownNow();
+    }
+
+    /**
+     * Creates a thread pool that can schedule commands to run after a given delay, or to execute periodically.
+     *
+     * @return Newly created thread pool.
+     */
+    protected ExecutorService newThreadPool() {
+        return Executors.newSingleThreadExecutor();
+    }
+
+    /**
+     * Execute command with specified arguments.
+     *
+     * @param args Map with method args.
+     */
+    public abstract Object execute(Map<String, Object> args) throws Exception;
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/ClusterListener.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/ClusterListener.java
new file mode 100644
index 0000000..9aa0e72
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/ClusterListener.java
@@ -0,0 +1,525 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.handlers;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.socket.client.Socket;
+import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.console.agent.AgentConfiguration;
+import org.apache.ignite.console.agent.rest.RestExecutor;
+import org.apache.ignite.console.agent.rest.RestResult;
+import org.apache.ignite.internal.processors.rest.client.message.GridClientNodeBean;
+import org.apache.ignite.internal.processors.rest.protocols.http.jetty.GridJettyObjectMapper;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.LT;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.lang.IgniteClosure;
+import org.apache.ignite.lang.IgniteProductVersion;
+import org.apache.ignite.logger.slf4j.Slf4jLogger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_CLUSTER_NAME;
+import static org.apache.ignite.console.agent.AgentUtils.toJSON;
+import static org.apache.ignite.internal.IgniteNodeAttributes.ATTR_BUILD_VER;
+import static org.apache.ignite.internal.IgniteNodeAttributes.ATTR_CLIENT_MODE;
+import static org.apache.ignite.internal.IgniteNodeAttributes.ATTR_IPS;
+import static org.apache.ignite.internal.processors.rest.GridRestResponse.STATUS_SUCCESS;
+import static org.apache.ignite.internal.processors.rest.client.message.GridClientResponse.STATUS_FAILED;
+import static org.apache.ignite.internal.visor.util.VisorTaskUtils.sortAddresses;
+import static org.apache.ignite.internal.visor.util.VisorTaskUtils.splitAddresses;
+
+/**
+ * API to transfer topology from Ignite cluster available by node-uri.
+ */
+public class ClusterListener implements AutoCloseable {
+    /** */
+    private static final IgniteLogger log = new Slf4jLogger(LoggerFactory.getLogger(ClusterListener.class));
+
+    /** */
+    private static final IgniteProductVersion IGNITE_2_0 = IgniteProductVersion.fromString("2.0.0");
+
+    /** */
+    private static final IgniteProductVersion IGNITE_2_1 = IgniteProductVersion.fromString("2.1.0");
+
+    /** */
+    private static final IgniteProductVersion IGNITE_2_3 = IgniteProductVersion.fromString("2.3.0");
+
+    /** Optional Ignite cluster ID. */
+    public static final String IGNITE_CLUSTER_ID = "IGNITE_CLUSTER_ID";
+
+    /** Unique Visor key to get events last order. */
+    private static final String EVT_LAST_ORDER_KEY = "WEB_AGENT_" + UUID.randomUUID().toString();
+
+    /** Unique Visor key to get events throttle counter. */
+    private static final String EVT_THROTTLE_CNTR_KEY = "WEB_AGENT_" + UUID.randomUUID().toString();
+
+    /** */
+    private static final String EVENT_CLUSTER_CONNECTED = "cluster:connected";
+
+    /** */
+    private static final String EVENT_CLUSTER_TOPOLOGY = "cluster:topology";
+
+    /** */
+    private static final String EVENT_CLUSTER_DISCONNECTED = "cluster:disconnected";
+
+    /** Topology refresh frequency. */
+    private static final long REFRESH_FREQ = 3000L;
+
+    /** JSON object mapper. */
+    private static final ObjectMapper MAPPER = new GridJettyObjectMapper();
+
+    /** Latest topology snapshot. */
+    private TopologySnapshot top;
+
+    /** */
+    private final WatchTask watchTask = new WatchTask();
+
+    /** */
+    private static final IgniteClosure<UUID, String> ID2ID8 = new IgniteClosure<UUID, String>() {
+        @Override public String apply(UUID nid) {
+            return U.id8(nid).toUpperCase();
+        }
+
+        @Override public String toString() {
+            return "Node ID to ID8 transformer closure.";
+        }
+    };
+
+    /** */
+    private final AgentConfiguration cfg;
+
+    /** */
+    private final Socket client;
+
+    /** */
+    private final RestExecutor restExecutor;
+
+    /** */
+    private static final ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
+
+    /** */
+    private ScheduledFuture<?> refreshTask;
+
+    /**
+     * @param client Client.
+     * @param restExecutor REST executor.
+     */
+    public ClusterListener(AgentConfiguration cfg, Socket client, RestExecutor restExecutor) {
+        this.cfg = cfg;
+        this.client = client;
+        this.restExecutor = restExecutor;
+    }
+
+    /**
+     * Callback on cluster connect.
+     *
+     * @param nids Cluster nodes IDs.
+     */
+    private void clusterConnect(Collection<UUID> nids) {
+        log.info("Connection successfully established to cluster with nodes: " + F.viewReadOnly(nids, ID2ID8));
+
+        client.emit(EVENT_CLUSTER_CONNECTED, toJSON(nids));
+    }
+
+    /**
+     * Callback on disconnect from cluster.
+     */
+    private void clusterDisconnect() {
+        if (top == null)
+            return;
+
+        top = null;
+
+        log.info("Connection to cluster was lost");
+
+        client.emit(EVENT_CLUSTER_DISCONNECTED);
+    }
+
+    /**
+     * Stop refresh task.
+     */
+    private void safeStopRefresh() {
+        if (refreshTask != null)
+            refreshTask.cancel(true);
+    }
+
+    /**
+     * Start watch cluster.
+     */
+    public void watch() {
+        safeStopRefresh();
+
+        refreshTask = pool.scheduleWithFixedDelay(watchTask, 0L, REFRESH_FREQ, TimeUnit.MILLISECONDS);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void close() {
+        refreshTask.cancel(true);
+
+        pool.shutdownNow();
+    }
+
+    /** */
+    private static class TopologySnapshot {
+        /** */
+        private String clusterId;
+
+        /** */
+        private String clusterName;
+
+        /** */
+        private Collection<UUID> nids;
+
+        /** */
+        private Map<UUID, String> addrs;
+
+        /** */
+        private Map<UUID, Boolean> clients;
+
+        /** */
+        private String clusterVerStr;
+
+        /** */
+        private IgniteProductVersion clusterVer;
+
+        /** */
+        private boolean active;
+
+        /** */
+        private boolean secured;
+
+        /**
+         * Helper method to get attribute.
+         *
+         * @param attrs Map with attributes.
+         * @param name Attribute name.
+         * @return Attribute value.
+         */
+        private static <T> T attribute(Map<String, Object> attrs, String name) {
+            return (T)attrs.get(name);
+        }
+
+        /**
+         * @param nodes Nodes.
+         */
+        TopologySnapshot(Collection<GridClientNodeBean> nodes) {
+            int sz = nodes.size();
+
+            nids = new ArrayList<>(sz);
+            addrs = U.newHashMap(sz);
+            clients = U.newHashMap(sz);
+            active = false;
+            secured = false;
+
+            for (GridClientNodeBean node : nodes) {
+                UUID nid = node.getNodeId();
+
+                nids.add(nid);
+
+                Map<String, Object> attrs = node.getAttributes();
+
+                if (F.isEmpty(clusterId))
+                    clusterId = attribute(attrs, IGNITE_CLUSTER_ID);
+
+                if (F.isEmpty(clusterName))
+                    clusterName = attribute(attrs, IGNITE_CLUSTER_NAME);
+
+                Boolean client = attribute(attrs, ATTR_CLIENT_MODE);
+
+                clients.put(nid, client);
+
+                Collection<String> nodeAddrs = client
+                    ? splitAddresses(attribute(attrs, ATTR_IPS))
+                    : node.getTcpAddresses();
+
+                String firstIP = F.first(sortAddresses(nodeAddrs));
+
+                addrs.put(nid, firstIP);
+
+                String nodeVerStr = attribute(attrs, ATTR_BUILD_VER);
+
+                IgniteProductVersion nodeVer = IgniteProductVersion.fromString(nodeVerStr);
+
+                if (clusterVer == null || clusterVer.compareTo(nodeVer) > 0) {
+                    clusterVer = nodeVer;
+                    clusterVerStr = nodeVerStr;
+                }
+            }
+        }
+
+        /**
+         * @return Cluster id.
+         */
+        public String getClusterId() {
+            return clusterId;
+        }
+
+        /**
+         * @return Cluster name.
+         */
+        public String getClusterName() {
+            return clusterName;
+        }
+
+        /**
+         * @return Cluster version.
+         */
+        public String getClusterVersion() {
+            return clusterVerStr;
+        }
+
+        /**
+         * @return Cluster active flag.
+         */
+        public boolean isActive() {
+            return active;
+        }
+
+        /**
+         * @param active New cluster active state.
+         */
+        public void setActive(boolean active) {
+            this.active = active;
+        }
+
+        /**
+         * @return {@code true} If cluster has configured security.
+         */
+        public boolean isSecured() {
+            return secured;
+        }
+
+        /**
+         * @param secured Configured security flag.
+         */
+        public void setSecured(boolean secured) {
+            this.secured = secured;
+        }
+
+        /**
+         * @return Cluster nodes IDs.
+         */
+        public Collection<UUID> getNids() {
+            return nids;
+        }
+
+        /**
+         * @return Cluster nodes with IPs.
+         */
+        public Map<UUID, String> getAddresses() {
+            return addrs;
+        }
+
+        /**
+         * @return Cluster nodes with client mode flag.
+         */
+        public Map<UUID, Boolean> getClients() {
+            return clients;
+        }
+
+        /**
+         * @return Cluster version.
+         */
+        public IgniteProductVersion clusterVersion() {
+            return clusterVer;
+        }
+
+        /**
+         * @return Collection of short UUIDs.
+         */
+        Collection<String> nid8() {
+            return F.viewReadOnly(nids, ID2ID8);
+        }
+
+        /**
+         * @param prev Previous topology.
+         * @return {@code true} in case if current topology is a new cluster.
+         */
+        boolean differentCluster(TopologySnapshot prev) {
+            return prev == null || F.isEmpty(prev.nids) || Collections.disjoint(nids, prev.nids);
+        }
+
+        /**
+         * @param prev Previous topology.
+         * @return {@code true} in case if current topology is the same cluster, but topology changed.
+         */
+        boolean topologyChanged(TopologySnapshot prev) {
+            return prev != null && !prev.nids.equals(nids);
+        }
+    }
+
+    /** */
+    private class WatchTask implements Runnable {
+        /** */
+        private static final String EXPIRED_SES_ERROR_MSG = "Failed to handle request - unknown session token (maybe expired session)";
+
+        /** */
+        private String sesTok;
+
+        /**
+         * Execute REST command under agent user.
+         *
+         * @param params Command params.
+         * @return Command result.
+         * @throws IOException If failed to execute.
+         */
+        private RestResult restCommand(Map<String, Object> params) throws IOException {
+            if (!F.isEmpty(sesTok))
+                params.put("sessionToken", sesTok);
+            else if (!F.isEmpty(cfg.nodeLogin()) && !F.isEmpty(cfg.nodePassword())) {
+                params.put("user", cfg.nodeLogin());
+                params.put("password", cfg.nodePassword());
+            }
+
+            RestResult res = restExecutor.sendRequest(cfg.nodeURIs(), params, null);
+
+            switch (res.getStatus()) {
+                case STATUS_SUCCESS:
+                    sesTok = res.getSessionToken();
+
+                    return res;
+
+                case STATUS_FAILED:
+                    if (res.getError().startsWith(EXPIRED_SES_ERROR_MSG)) {
+                        sesTok = null;
+
+                        params.remove("sessionToken");
+
+                        return restCommand(params);
+                    }
+
+                default:
+                    return res;
+            }
+        }
+
+        /**
+         * Collect topology.
+         *
+         * @return REST result.
+         * @throws IOException If failed to collect topology.
+         */
+        private RestResult topology() throws IOException {
+            Map<String, Object> params = U.newHashMap(4);
+
+            params.put("cmd", "top");
+            params.put("attr", true);
+            params.put("mtr", false);
+            params.put("caches", false);
+
+            return restCommand(params);
+        }
+
+        /**
+         * @param ver Cluster version.
+         * @param nid Node ID.
+         * @return Cluster active state.
+         * @throws IOException If failed to collect cluster active state.
+         */
+        public boolean active(IgniteProductVersion ver, UUID nid) throws IOException {
+            // 1.x clusters are always active.
+            if (ver.compareTo(IGNITE_2_0) < 0)
+                return true;
+
+            Map<String, Object> params = U.newHashMap(10);
+
+            boolean v23 = ver.compareTo(IGNITE_2_3) >= 0;
+
+            if (v23)
+                params.put("cmd", "currentState");
+            else {
+                params.put("cmd", "exe");
+                params.put("name", "org.apache.ignite.internal.visor.compute.VisorGatewayTask");
+                params.put("p1", nid);
+                params.put("p2", "org.apache.ignite.internal.visor.node.VisorNodeDataCollectorTask");
+                params.put("p3", "org.apache.ignite.internal.visor.node.VisorNodeDataCollectorTaskArg");
+                params.put("p4", false);
+                params.put("p5", EVT_LAST_ORDER_KEY);
+                params.put("p6", EVT_THROTTLE_CNTR_KEY);
+
+                if (ver.compareTo(IGNITE_2_1) >= 0)
+                    params.put("p7", false);
+                else {
+                    params.put("p7", 10);
+                    params.put("p8", false);
+                }
+            }
+
+            RestResult res = restCommand(params);
+
+            if (res.getStatus() == STATUS_SUCCESS)
+                return v23 ? Boolean.valueOf(res.getData()) : res.getData().contains("\"active\":true");
+
+            throw new IOException(res.getError());
+        }
+
+        /** {@inheritDoc} */
+        @Override public void run() {
+            try {
+                RestResult res = topology();
+
+                if (res.getStatus() == STATUS_SUCCESS) {
+                    List<GridClientNodeBean> nodes = MAPPER.readValue(res.getData(),
+                        new TypeReference<List<GridClientNodeBean>>() {});
+
+                    TopologySnapshot newTop = new TopologySnapshot(nodes);
+
+                    if (newTop.differentCluster(top))
+                        log.info("Connection successfully established to cluster with nodes: " + newTop.nid8());
+                    else if (newTop.topologyChanged(top))
+                        log.info("Cluster topology changed, new topology: " + newTop.nid8());
+
+                    boolean active = active(newTop.clusterVersion(), F.first(newTop.getNids()));
+
+                    newTop.setActive(active);
+                    newTop.setSecured(!F.isEmpty(res.getSessionToken()));
+
+                    top = newTop;
+
+                    client.emit(EVENT_CLUSTER_TOPOLOGY, toJSON(top));
+                }
+                else {
+                    LT.warn(log, res.getError());
+
+                    clusterDisconnect();
+                }
+            }
+            catch (ConnectException ignored) {
+                clusterDisconnect();
+            }
+            catch (Throwable e) {
+                log.error("WatchTask failed", e);
+
+                clusterDisconnect();
+            }
+        }
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/DatabaseListener.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/DatabaseListener.java
new file mode 100644
index 0000000..d794297
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/DatabaseListener.java
@@ -0,0 +1,354 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.handlers;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import io.socket.emitter.Emitter;
+import org.apache.ignite.console.agent.AgentConfiguration;
+import org.apache.ignite.console.agent.db.DbMetadataReader;
+import org.apache.ignite.console.agent.db.DbSchema;
+import org.apache.ignite.console.agent.db.DbTable;
+import org.apache.ignite.console.demo.AgentMetadataDemo;
+import org.apache.log4j.Logger;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.ignite.console.agent.AgentUtils.resolvePath;
+
+/**
+ * API to extract database metadata.
+ */
+public class DatabaseListener {
+    /** */
+    private static final Logger log = Logger.getLogger(DatabaseListener.class.getName());
+
+    /** */
+    private static final String IMPLEMENTATION_VERSION = "Implementation-Version";
+
+    /** */
+    private static final String BUNDLE_VERSION = "Bundle-Version";
+
+    /** */
+    private final File driversFolder;
+
+    /** */
+    private final DbMetadataReader dbMetaReader;
+
+    /** */
+    private final AbstractListener schemasLsnr = new AbstractListener() {
+        @Override public Object execute(Map<String, Object> args) throws Exception {
+            String driverPath = null;
+
+            if (args.containsKey("jdbcDriverJar"))
+                driverPath = args.get("jdbcDriverJar").toString();
+
+            if (!args.containsKey("jdbcDriverClass"))
+                throw new IllegalArgumentException("Missing driverClass in arguments: " + args);
+
+            String driverCls = args.get("jdbcDriverClass").toString();
+
+            if (!args.containsKey("jdbcUrl"))
+                throw new IllegalArgumentException("Missing url in arguments: " + args);
+
+            String url = args.get("jdbcUrl").toString();
+
+            if (!args.containsKey("info"))
+                throw new IllegalArgumentException("Missing info in arguments: " + args);
+
+            Properties info = new Properties();
+
+            info.putAll((Map)args.get("info"));
+
+            return schemas(driverPath, driverCls, url, info);
+        }
+    };
+
+    /** */
+    private final AbstractListener metadataLsnr = new AbstractListener() {
+        @Override public Object execute(Map<String, Object> args) throws Exception {
+            String driverPath = null;
+
+            if (args.containsKey("jdbcDriverJar"))
+                driverPath = args.get("jdbcDriverJar").toString();
+
+            if (!args.containsKey("jdbcDriverClass"))
+                throw new IllegalArgumentException("Missing driverClass in arguments: " + args);
+
+            String driverCls = args.get("jdbcDriverClass").toString();
+
+            if (!args.containsKey("jdbcUrl"))
+                throw new IllegalArgumentException("Missing url in arguments: " + args);
+
+            String url = args.get("jdbcUrl").toString();
+
+            if (!args.containsKey("info"))
+                throw new IllegalArgumentException("Missing info in arguments: " + args);
+
+            Properties info = new Properties();
+
+            info.putAll((Map)args.get("info"));
+
+            if (!args.containsKey("schemas"))
+                throw new IllegalArgumentException("Missing schemas in arguments: " + args);
+
+            List<String> schemas = (List<String>)args.get("schemas");
+
+            if (!args.containsKey("tablesOnly"))
+                throw new IllegalArgumentException("Missing tablesOnly in arguments: " + args);
+
+            boolean tblsOnly = (boolean)args.get("tablesOnly");
+
+            return metadata(driverPath, driverCls, url, info, schemas, tblsOnly);
+        }
+    };
+
+    /** */
+    private final AbstractListener availableDriversLsnr = new AbstractListener() {
+        @Override public Object execute(Map<String, Object> args) {
+            if (driversFolder == null) {
+                log.info("JDBC drivers folder not specified, returning empty list");
+
+                return Collections.emptyList();
+            }
+
+            if (log.isDebugEnabled())
+                log.debug("Collecting JDBC drivers in folder: " + driversFolder.getPath());
+
+            File[] list = driversFolder.listFiles(new FilenameFilter() {
+                @Override public boolean accept(File dir, String name) {
+                    return name.endsWith(".jar");
+                }
+            });
+
+            if (list == null) {
+                log.info("JDBC drivers folder has no files, returning empty list");
+
+                return Collections.emptyList();
+            }
+
+            List<JdbcDriver> res = new ArrayList<>();
+
+            for (File file : list) {
+                try {
+                    boolean win = System.getProperty("os.name").contains("win");
+
+                    URL url = new URL("jar", null,
+                        "file:" + (win ? "/" : "") + file.getPath() + "!/META-INF/services/java.sql.Driver");
+
+                    try (
+                        BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), UTF_8));
+                        JarFile jar = new JarFile(file.getPath())
+                    ) {
+                        Manifest m = jar.getManifest();
+                        Object ver = m.getMainAttributes().getValue(IMPLEMENTATION_VERSION);
+
+                        if (ver == null)
+                            ver = m.getMainAttributes().getValue(BUNDLE_VERSION);
+
+                        String jdbcDriverCls = reader.readLine();
+
+                        res.add(new JdbcDriver(file.getName(), jdbcDriverCls, ver != null ? ver.toString() : null));
+
+                        if (log.isDebugEnabled())
+                            log.debug("Found: [driver=" + file + ", class=" + jdbcDriverCls + "]");
+                    }
+                }
+                catch (IOException e) {
+                    res.add(new JdbcDriver(file.getName(), null, null));
+
+                    log.info("Found: [driver=" + file + "]");
+                    log.info("Failed to detect driver class: " + e.getMessage());
+                }
+            }
+
+            return res;
+        }
+    };
+
+    /**
+     * @param cfg Config.
+     */
+    public DatabaseListener(AgentConfiguration cfg) {
+        driversFolder = resolvePath(cfg.driversFolder() == null ? "jdbc-drivers" : cfg.driversFolder());
+        dbMetaReader = new DbMetadataReader();
+    }
+
+    /**
+     * @param jdbcDriverJarPath JDBC driver JAR path.
+     * @param jdbcDriverCls JDBC driver class.
+     * @param jdbcUrl JDBC URL.
+     * @param jdbcInfo Properties to connect to database.
+     * @return Connection to database.
+     * @throws SQLException If failed to connect.
+     */
+    private Connection connect(String jdbcDriverJarPath, String jdbcDriverCls, String jdbcUrl,
+        Properties jdbcInfo) throws SQLException {
+        if (AgentMetadataDemo.isTestDriveUrl(jdbcUrl))
+            return AgentMetadataDemo.testDrive();
+
+        if (!new File(jdbcDriverJarPath).isAbsolute() && driversFolder != null)
+            jdbcDriverJarPath = new File(driversFolder, jdbcDriverJarPath).getPath();
+
+        return dbMetaReader.connect(jdbcDriverJarPath, jdbcDriverCls, jdbcUrl, jdbcInfo);
+    }
+
+    /**
+     * @param jdbcDriverJarPath JDBC driver JAR path.
+     * @param jdbcDriverCls JDBC driver class.
+     * @param jdbcUrl JDBC URL.
+     * @param jdbcInfo Properties to connect to database.
+     * @return Collection of schema names.
+     * @throws SQLException If failed to collect schemas.
+     */
+    protected DbSchema schemas(String jdbcDriverJarPath, String jdbcDriverCls, String jdbcUrl,
+        Properties jdbcInfo) throws SQLException {
+        if (log.isDebugEnabled())
+            log.debug("Start collecting database schemas [drvJar=" + jdbcDriverJarPath +
+                ", drvCls=" + jdbcDriverCls + ", jdbcUrl=" + jdbcUrl + "]");
+
+        try (Connection conn = connect(jdbcDriverJarPath, jdbcDriverCls, jdbcUrl, jdbcInfo)) {
+            String catalog = conn.getCatalog();
+
+            if (catalog == null) {
+                String[] parts = jdbcUrl.split("[/:=]");
+
+                catalog = parts.length > 0 ? parts[parts.length - 1] : "NONE";
+            }
+
+            Collection<String> schemas = dbMetaReader.schemas(conn);
+
+            if (log.isDebugEnabled())
+                log.debug("Finished collection of schemas [jdbcUrl=" + jdbcUrl + ", catalog=" + catalog +
+                    ", count=" + schemas.size() + "]");
+
+            return new DbSchema(catalog, schemas);
+        }
+        catch (Throwable e) {
+            log.error("Failed to collect schemas", e);
+
+            throw new SQLException("Failed to collect schemas", e);
+        }
+    }
+
+    /**
+     * Listener for drivers.
+     *
+     * @return Drivers in drivers folder
+     * @see AgentConfiguration#driversFolder
+     */
+    public Emitter.Listener availableDriversListener() {
+        return availableDriversLsnr;
+    }
+
+    /**
+     * Listener for schema names.
+     *
+     * @return Collection of schema names.
+     */
+    public Emitter.Listener schemasListener() {
+        return schemasLsnr;
+    }
+
+    /**
+     * @param jdbcDriverJarPath JDBC driver JAR path.
+     * @param jdbcDriverCls JDBC driver class.
+     * @param jdbcUrl JDBC URL.
+     * @param jdbcInfo Properties to connect to database.
+     * @param schemas List of schema names to process.
+     * @param tblsOnly If {@code true} then only tables will be processed otherwise views also will be processed.
+     * @return Collection of tables.
+     */
+    protected Collection<DbTable> metadata(String jdbcDriverJarPath, String jdbcDriverCls, String jdbcUrl,
+        Properties jdbcInfo, List<String> schemas, boolean tblsOnly) throws SQLException {
+        if (log.isDebugEnabled())
+            log.debug("Start collecting database metadata [drvJar=" + jdbcDriverJarPath +
+                ", drvCls=" + jdbcDriverCls + ", jdbcUrl=" + jdbcUrl + "]");
+
+        try (Connection conn = connect(jdbcDriverJarPath, jdbcDriverCls, jdbcUrl, jdbcInfo)) {
+            Collection<DbTable> metadata = dbMetaReader.metadata(conn, schemas, tblsOnly);
+
+            if (log.isDebugEnabled())
+                log.debug("Finished collection of metadata [jdbcUrl=" + jdbcUrl + ", count=" + metadata.size() + "]");
+
+            return metadata;
+        }
+        catch (Throwable e) {
+            log.error("Failed to collect metadata", e);
+
+            throw new SQLException("Failed to collect metadata", e);
+        }
+    }
+
+    /**
+     * Listener for tables.
+     *
+     * @return Collection of tables.
+     */
+    public Emitter.Listener metadataListener() {
+        return metadataLsnr;
+    }
+
+    /**
+     * Stop handler.
+     */
+    public void stop() {
+        availableDriversLsnr.stop();
+
+        schemasLsnr.stop();
+
+        metadataLsnr.stop();
+    }
+
+    /**
+     * Wrapper class for later to be transformed to JSON and send to Web Console.
+     */
+    private static class JdbcDriver {
+        /** */
+        public final String jdbcDriverJar;
+
+        /** */
+        public final String jdbcDriverCls;
+
+        /** */
+        public final String jdbcDriverImplVersion;
+
+        /**
+         * @param jdbcDriverJar File name of driver jar file.
+         * @param jdbcDriverCls Optional JDBC driver class.
+         */
+        public JdbcDriver(String jdbcDriverJar, String jdbcDriverCls, String jdbcDriverImplVersion) {
+            this.jdbcDriverJar = jdbcDriverJar;
+            this.jdbcDriverCls = jdbcDriverCls;
+            this.jdbcDriverImplVersion = jdbcDriverImplVersion;
+        }
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/RestListener.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/RestListener.java
new file mode 100644
index 0000000..89fac44
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/RestListener.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.handlers;
+
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.apache.ignite.console.agent.AgentConfiguration;
+import org.apache.ignite.console.agent.rest.RestExecutor;
+import org.apache.ignite.console.agent.rest.RestResult;
+import org.apache.ignite.console.demo.AgentClusterDemo;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.U;
+
+/**
+ * API to translate REST requests to Ignite cluster.
+ */
+public class RestListener extends AbstractListener {
+    /** */
+    private final AgentConfiguration cfg;
+
+    /** */
+    private final RestExecutor restExecutor;
+
+    /**
+     * @param cfg Config.
+     * @param restExecutor REST executor.
+     */
+    public RestListener(AgentConfiguration cfg, RestExecutor restExecutor) {
+        this.cfg = cfg;
+        this.restExecutor = restExecutor;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected ExecutorService newThreadPool() {
+        return Executors.newCachedThreadPool();
+    }
+
+    /** {@inheritDoc} */
+    @Override public Object execute(Map<String, Object> args) {
+        if (log.isDebugEnabled())
+            log.debug("Start parse REST command args: " + args);
+
+        Map<String, Object> params = null;
+
+        if (args.containsKey("params"))
+            params = (Map<String, Object>)args.get("params");
+
+        if (!args.containsKey("demo"))
+            throw new IllegalArgumentException("Missing demo flag in arguments: " + args);
+
+        boolean demo = (boolean)args.get("demo");
+
+        if (F.isEmpty((String)args.get("token")))
+            return RestResult.fail(401, "Request does not contain user token.");
+
+        Map<String, Object> headers = null;
+
+        if (args.containsKey("headers"))
+            headers = (Map<String, Object>)args.get("headers");
+
+        try {
+            if (demo) {
+                if (AgentClusterDemo.getDemoUrl() == null) {
+                    if (cfg.disableDemo())
+                        return RestResult.fail(404, "Demo mode disabled by administrator.");
+
+                    AgentClusterDemo.tryStart().await();
+
+                    if (AgentClusterDemo.getDemoUrl() == null)
+                        return RestResult.fail(404, "Failed to send request because of embedded node for demo mode is not started yet.");
+                }
+
+                return restExecutor.sendRequest(AgentClusterDemo.getDemoUrl(), params, headers);
+            }
+
+            return restExecutor.sendRequest(this.cfg.nodeURIs(), params, headers);
+        }
+        catch (Exception e) {
+            U.error(log, "Failed to execute REST command with parameters: " + params, e);
+
+            return RestResult.fail(404, e.getMessage());
+        }
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/rest/RestExecutor.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/rest/RestExecutor.java
new file mode 100644
index 0000000..f8455f3
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/rest/RestExecutor.java
@@ -0,0 +1,409 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.rest;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.net.ConnectException;
+import java.security.GeneralSecurityException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import okhttp3.Dispatcher;
+import okhttp3.FormBody;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.internal.processors.rest.protocols.http.jetty.GridJettyObjectMapper;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.LT;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.logger.slf4j.Slf4jLogger;
+import org.slf4j.LoggerFactory;
+
+import static com.fasterxml.jackson.core.JsonToken.END_ARRAY;
+import static com.fasterxml.jackson.core.JsonToken.END_OBJECT;
+import static com.fasterxml.jackson.core.JsonToken.START_ARRAY;
+import static org.apache.ignite.console.agent.AgentUtils.sslConnectionSpec;
+import static org.apache.ignite.console.agent.AgentUtils.sslSocketFactory;
+import static org.apache.ignite.console.agent.AgentUtils.trustManager;
+import static org.apache.ignite.internal.processors.rest.GridRestResponse.STATUS_AUTH_FAILED;
+import static org.apache.ignite.internal.processors.rest.GridRestResponse.STATUS_FAILED;
+import static org.apache.ignite.internal.processors.rest.GridRestResponse.STATUS_SUCCESS;
+
+/**
+ * API to translate REST requests to Ignite cluster.
+ */
+public class RestExecutor implements AutoCloseable {
+    /** */
+    private static final IgniteLogger log = new Slf4jLogger(LoggerFactory.getLogger(RestExecutor.class));
+
+    /** JSON object mapper. */
+    private static final ObjectMapper MAPPER = new GridJettyObjectMapper();
+
+    /** */
+    private final OkHttpClient httpClient;
+
+    /** Index of alive node URI. */
+    private final Map<List<String>, Integer> startIdxs = U.newHashMap(2);
+
+    /**
+     * Constructor.
+     *
+     * @param trustAll {@code true} If we trust to self-signed sertificates.
+     * @param keyStorePath Optional path to key store file.
+     * @param keyStorePwd Optional password for key store.
+     * @param trustStorePath Optional path to trust store file.
+     * @param trustStorePwd Optional password for trust store.
+     * @param cipherSuites Optional cipher suites.
+     * @throws GeneralSecurityException If failed to initialize SSL.
+     * @throws IOException If failed to load content of key stores.
+     */
+    public RestExecutor(
+        boolean trustAll,
+        String keyStorePath,
+        String keyStorePwd,
+        String trustStorePath,
+        String trustStorePwd,
+        List<String> cipherSuites
+    ) throws GeneralSecurityException, IOException {
+        Dispatcher dispatcher = new Dispatcher();
+
+        dispatcher.setMaxRequests(Integer.MAX_VALUE);
+        dispatcher.setMaxRequestsPerHost(Integer.MAX_VALUE);
+
+        OkHttpClient.Builder builder = new OkHttpClient.Builder()
+            .readTimeout(0, TimeUnit.MILLISECONDS)
+            .dispatcher(dispatcher);
+
+        X509TrustManager trustMgr = trustManager(trustAll, trustStorePath, trustStorePwd);
+
+        if (trustAll)
+            builder.hostnameVerifier((hostname, session) -> true);
+
+        SSLSocketFactory sslSocketFactory = sslSocketFactory(
+            keyStorePath, keyStorePwd,
+            trustMgr,
+            cipherSuites
+        );
+
+        if (sslSocketFactory != null) {
+            if (trustMgr != null)
+                builder.sslSocketFactory(sslSocketFactory, trustMgr);
+            else
+                builder.sslSocketFactory(sslSocketFactory);
+
+            if (!F.isEmpty(cipherSuites))
+                builder.connectionSpecs(sslConnectionSpec(cipherSuites));
+        }
+
+        httpClient = builder.build();
+    }
+
+    /**
+     * Stop HTTP client.
+     */
+    @Override public void close() {
+        if (httpClient != null) {
+            httpClient.dispatcher().executorService().shutdown();
+
+            httpClient.dispatcher().cancelAll();
+        }
+    }
+
+    /** */
+    private RestResult parseResponse(Response res) throws IOException {
+        if (res.isSuccessful()) {
+            RestResponseHolder holder = MAPPER.readValue(res.body().byteStream(), RestResponseHolder.class);
+
+            int status = holder.getSuccessStatus();
+
+            if (status == STATUS_SUCCESS)
+                return RestResult.success(holder.getResponse(), holder.getSessionToken());
+
+            return RestResult.fail(status, holder.getError());
+        }
+
+        if (res.code() == 401)
+            return RestResult.fail(STATUS_AUTH_FAILED, "Failed to authenticate in cluster. " +
+                "Please check agent\'s login and password or node port.");
+
+        if (res.code() == 404)
+            return RestResult.fail(STATUS_FAILED, "Failed connect to cluster.");
+
+        return RestResult.fail(STATUS_FAILED, "Failed to execute REST command: " + res);
+    }
+
+    /** */
+    private RestResult sendRequest(String url, Map<String, Object> params, Map<String, Object> headers) throws IOException {
+        HttpUrl httpUrl = HttpUrl
+            .parse(url)
+            .newBuilder()
+            .addPathSegment("ignite")
+            .build();
+
+        final Request.Builder reqBuilder = new Request.Builder();
+
+        if (headers != null) {
+            for (Map.Entry<String, Object> entry : headers.entrySet())
+                if (entry.getValue() != null)
+                    reqBuilder.addHeader(entry.getKey(), entry.getValue().toString());
+        }
+
+        FormBody.Builder bodyParams = new FormBody.Builder();
+
+        if (params != null) {
+            for (Map.Entry<String, Object> entry : params.entrySet()) {
+                if (entry.getValue() != null)
+                    bodyParams.add(entry.getKey(), entry.getValue().toString());
+            }
+        }
+
+        reqBuilder.url(httpUrl).post(bodyParams.build());
+
+        try (Response resp = httpClient.newCall(reqBuilder.build()).execute()) {
+            return parseResponse(resp);
+        }
+    }
+
+    /**
+     * Send request to cluster.
+     *
+     * @param nodeURIs List of cluster nodes URIs.
+     * @param params Map with reques params.
+     * @param headers Map with reques headers.
+     * @return Response from cluster.
+     * @throws IOException If failed to send request to cluster.
+     */
+    public RestResult sendRequest(
+        List<String> nodeURIs,
+        Map<String, Object> params,
+        Map<String, Object> headers
+    ) throws IOException {
+        Integer startIdx = startIdxs.getOrDefault(nodeURIs, 0);
+
+        int urlsCnt = nodeURIs.size();
+
+        for (int i = 0; i < urlsCnt; i++) {
+            Integer currIdx = (startIdx + i) % urlsCnt;
+
+            String nodeUrl = nodeURIs.get(currIdx);
+
+            try {
+                RestResult res = sendRequest(nodeUrl, params, headers);
+
+                // If first attempt failed then throttling should be cleared.
+                if (i > 0)
+                    LT.clear();
+
+                LT.info(log, "Connected to cluster [url=" + nodeUrl + "]");
+
+                startIdxs.put(nodeURIs, currIdx);
+
+                return res;
+            }
+            catch (ConnectException ignored) {
+                LT.warn(log, "Failed connect to cluster [url=" + nodeUrl + "]");
+            }
+        }
+
+        LT.warn(log, "Failed connect to cluster. " +
+            "Please ensure that nodes have [ignite-rest-http] module in classpath " +
+            "(was copied from libs/optional to libs folder).");
+
+        throw new ConnectException("Failed connect to cluster [urls=" + nodeURIs + ", parameters=" + params + "]");
+    }
+
+    /**
+     * REST response holder Java bean.
+     */
+    private static class RestResponseHolder {
+        /** Success flag */
+        private int successStatus;
+
+        /** Error. */
+        private String err;
+
+        /** Response. */
+        private String res;
+
+        /** Session token string representation. */
+        private String sesTok;
+
+        /**
+         * @return {@code True} if this request was successful.
+         */
+        public int getSuccessStatus() {
+            return successStatus;
+        }
+
+        /**
+         * @param successStatus Whether request was successful.
+         */
+        public void setSuccessStatus(int successStatus) {
+            this.successStatus = successStatus;
+        }
+
+        /**
+         * @return Error.
+         */
+        public String getError() {
+            return err;
+        }
+
+        /**
+         * @param err Error.
+         */
+        public void setError(String err) {
+            this.err = err;
+        }
+
+        /**
+         * @return Response object.
+         */
+        public String getResponse() {
+            return res;
+        }
+
+        /**
+         * @param res Response object.
+         */
+        @JsonDeserialize(using = RawContentDeserializer.class)
+        public void setResponse(String res) {
+            this.res = res;
+        }
+
+        /**
+         * @return String representation of session token.
+         */
+        public String getSessionToken() {
+            return sesTok;
+        }
+
+        /**
+         * @param sesTok String representation of session token.
+         */
+        public void setSessionToken(String sesTok) {
+            this.sesTok = sesTok;
+        }
+    }
+
+    /**
+     * Raw content deserializer that will deserialize any data as string.
+     */
+    private static class RawContentDeserializer extends JsonDeserializer<String> {
+        /** */
+        private final JsonFactory factory = new JsonFactory();
+
+        /**
+         * @param tok Token to process.
+         * @param p Parser.
+         * @param gen Generator.
+         */
+        private void writeToken(JsonToken tok, JsonParser p, JsonGenerator gen) throws IOException {
+            switch (tok) {
+                case FIELD_NAME:
+                    gen.writeFieldName(p.getText());
+                    break;
+
+                case START_ARRAY:
+                    gen.writeStartArray();
+                    break;
+
+                case END_ARRAY:
+                    gen.writeEndArray();
+                    break;
+
+                case START_OBJECT:
+                    gen.writeStartObject();
+                    break;
+
+                case END_OBJECT:
+                    gen.writeEndObject();
+                    break;
+
+                case VALUE_NUMBER_INT:
+                    gen.writeNumber(p.getBigIntegerValue());
+                    break;
+
+                case VALUE_NUMBER_FLOAT:
+                    gen.writeNumber(p.getDecimalValue());
+                    break;
+
+                case VALUE_TRUE:
+                    gen.writeBoolean(true);
+                    break;
+
+                case VALUE_FALSE:
+                    gen.writeBoolean(false);
+                    break;
+
+                case VALUE_NULL:
+                    gen.writeNull();
+                    break;
+
+                default:
+                    gen.writeString(p.getText());
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+            JsonToken startTok = p.getCurrentToken();
+
+            if (startTok.isStructStart()) {
+                StringWriter wrt = new StringWriter(4096);
+
+                JsonGenerator gen = factory.createGenerator(wrt);
+
+                JsonToken tok = startTok, endTok = startTok == START_ARRAY ? END_ARRAY : END_OBJECT;
+
+                int cnt = 1;
+
+                while (cnt > 0) {
+                    writeToken(tok, p, gen);
+
+                    tok = p.nextToken();
+
+                    if (tok == startTok)
+                        cnt++;
+                    else if (tok == endTok)
+                        cnt--;
+                }
+
+                gen.close();
+
+                return wrt.toString();
+            }
+
+            return p.getValueAsString();
+        }
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/agent/rest/RestResult.java b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/rest/RestResult.java
new file mode 100644
index 0000000..fc8b4e9
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/agent/rest/RestResult.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.rest;
+
+/**
+ * Request result.
+ */
+public class RestResult {
+    /** REST http code. */
+    private int status;
+
+    /** The field contains description of error if server could not handle the request. */
+    private String error;
+
+    /** The field contains result of command. */
+    private String data;
+
+    /** Session token string representation. */
+    private String sesTok;
+
+    /** Flag of zipped data. */
+    private boolean zipped;
+
+    /**
+     * @param status REST http code.
+     * @param error The field contains description of error if server could not handle the request.
+     * @param data The field contains result of command.
+     */
+    private RestResult(int status, String error, String data) {
+        this.status = status;
+        this.error = error;
+        this.data = data;
+    }
+
+    /**
+     * @param status REST http code.
+     * @param error The field contains description of error if server could not handle the request.
+     * @return Request result.
+     */
+    public static RestResult fail(int status, String error) {
+        return new RestResult(status, error, null);
+    }
+
+    /**
+     * @param data The field contains result of command.
+     * @return Request result.
+     */
+    public static RestResult success(String data, String sesTok) {
+        RestResult res = new RestResult(0, null, data);
+
+        res.sesTok = sesTok;
+
+        return res;
+    }
+
+    /**
+     * @return REST http code.
+     */
+    public int getStatus() {
+        return status;
+    }
+
+    /**
+     * @return The field contains description of error if server could not handle the request.
+     */
+    public String getError() {
+        return error;
+    }
+
+    /**
+     * @return The field contains result of command.
+     */
+    public String getData() {
+        return data;
+    }
+
+    /**
+     * @return String representation of session token.
+     */
+    public String getSessionToken() {
+        return sesTok;
+    }
+
+    /**
+     * @param data Set zipped data.
+     */
+    public void zipData(String data) {
+        zipped = true;
+
+        this.data = data;
+    }
+
+    /**
+     * @return {@code true if data is zipped and Base64 encoded.}
+     */
+    public boolean isZipped() {
+        return zipped;
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/AgentClusterDemo.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/AgentClusterDemo.java
new file mode 100644
index 0000000..fac9241
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/AgentClusterDemo.java
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo;
+
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.IgniteServices;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.console.demo.service.DemoCachesLoadService;
+import org.apache.ignite.console.demo.service.DemoComputeLoadService;
+import org.apache.ignite.console.demo.service.DemoRandomCacheLoadService;
+import org.apache.ignite.console.demo.service.DemoServiceClusterSingleton;
+import org.apache.ignite.console.demo.service.DemoServiceKeyAffinity;
+import org.apache.ignite.console.demo.service.DemoServiceMultipleInstances;
+import org.apache.ignite.console.demo.service.DemoServiceNodeSingleton;
+import org.apache.ignite.internal.processors.cache.persistence.filename.PdsConsistentIdProcessor;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.logger.slf4j.Slf4jLogger;
+import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
+import org.apache.ignite.spi.eventstorage.memory.MemoryEventStorageSpi;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_ATOMIC_CACHE_DELETE_HISTORY_SIZE;
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_JETTY_PORT;
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_NO_ASCII;
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_PERFORMANCE_SUGGESTIONS_DISABLED;
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_QUIET;
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_UPDATE_NOTIFIER;
+import static org.apache.ignite.configuration.DataStorageConfiguration.DFLT_DATA_REGION_INITIAL_SIZE;
+import static org.apache.ignite.configuration.WALMode.LOG_ONLY;
+import static org.apache.ignite.console.demo.AgentDemoUtils.newScheduledThreadPool;
+import static org.apache.ignite.events.EventType.EVTS_DISCOVERY;
+import static org.apache.ignite.internal.IgniteNodeAttributes.ATTR_REST_JETTY_ADDRS;
+import static org.apache.ignite.internal.IgniteNodeAttributes.ATTR_REST_JETTY_PORT;
+import static org.apache.ignite.internal.visor.util.VisorTaskUtils.VISOR_TASK_EVTS;
+
+/**
+ * Demo for cluster features like SQL and Monitoring.
+ *
+ * Cache will be created and populated with data to query.
+ */
+public class AgentClusterDemo {
+    /** */
+    private static final Logger log = LoggerFactory.getLogger(AgentClusterDemo.class);
+
+    /** */
+    private static final AtomicBoolean initGuard = new AtomicBoolean();
+
+    /** */
+    private static final String SRV_NODE_NAME = "demo-server-";
+
+    /** */
+    private static final String CLN_NODE_NAME = "demo-client-";
+
+    /** */
+    private static final int NODE_CNT = 3;
+
+    /** */
+    private static final int WAL_SEGMENTS = 5;
+
+    /** WAL file segment size, 16MBytes. */
+    private static final int WAL_SEGMENT_SZ = 16 * 1024 * 1024;
+
+    /** */
+    private static CountDownLatch initLatch = new CountDownLatch(1);
+
+    /** */
+    private static volatile List<String> demoUrl;
+
+    /**
+     * Configure node.
+     *
+     * @param basePort Base port.
+     * @param gridIdx Ignite instance name index.
+     * @param client If {@code true} then start client node.
+     * @return IgniteConfiguration
+     */
+    private static IgniteConfiguration igniteConfiguration(int basePort, int gridIdx, boolean client)
+        throws IgniteCheckedException {
+        IgniteConfiguration cfg = new IgniteConfiguration();
+
+        cfg.setGridLogger(new Slf4jLogger());
+
+        cfg.setIgniteInstanceName((client ? CLN_NODE_NAME : SRV_NODE_NAME) + gridIdx);
+        cfg.setLocalHost("127.0.0.1");
+        cfg.setEventStorageSpi(new MemoryEventStorageSpi());
+        cfg.setConsistentId(cfg.getIgniteInstanceName());
+
+        File workDir = new File(U.workDirectory(null, null), "demo-work");
+
+        cfg.setWorkDirectory(workDir.getAbsolutePath());
+
+        int[] evts = new int[EVTS_DISCOVERY.length + VISOR_TASK_EVTS.length];
+
+        System.arraycopy(EVTS_DISCOVERY, 0, evts, 0, EVTS_DISCOVERY.length);
+        System.arraycopy(VISOR_TASK_EVTS, 0, evts, EVTS_DISCOVERY.length, VISOR_TASK_EVTS.length);
+
+        cfg.setIncludeEventTypes(evts);
+
+        cfg.getConnectorConfiguration().setPort(basePort);
+
+        System.setProperty(IGNITE_JETTY_PORT, String.valueOf(basePort + 10));
+
+        TcpDiscoveryVmIpFinder ipFinder = new TcpDiscoveryVmIpFinder();
+
+        int discoPort = basePort + 20;
+
+        ipFinder.setAddresses(Collections.singletonList("127.0.0.1:" + discoPort + ".." + (discoPort + NODE_CNT - 1)));
+
+        // Configure discovery SPI.
+        TcpDiscoverySpi discoSpi = new TcpDiscoverySpi();
+
+        discoSpi.setLocalPort(discoPort);
+        discoSpi.setIpFinder(ipFinder);
+
+        cfg.setDiscoverySpi(discoSpi);
+
+        TcpCommunicationSpi commSpi = new TcpCommunicationSpi();
+
+        commSpi.setSharedMemoryPort(-1);
+        commSpi.setMessageQueueLimit(10);
+
+        int commPort = basePort + 30;
+
+        commSpi.setLocalPort(commPort);
+
+        cfg.setCommunicationSpi(commSpi);
+        cfg.setGridLogger(new Slf4jLogger(log));
+        cfg.setMetricsLogFrequency(0);
+
+        DataRegionConfiguration dataRegCfg = new DataRegionConfiguration();
+        dataRegCfg.setName("demo");
+        dataRegCfg.setMetricsEnabled(true);
+        dataRegCfg.setMaxSize(DFLT_DATA_REGION_INITIAL_SIZE);
+        dataRegCfg.setPersistenceEnabled(true);
+
+        DataStorageConfiguration dataStorageCfg = new DataStorageConfiguration();
+        dataStorageCfg.setMetricsEnabled(true);
+        dataStorageCfg.setStoragePath(PdsConsistentIdProcessor.DB_DEFAULT_FOLDER);
+        dataStorageCfg.setDefaultDataRegionConfiguration(dataRegCfg);
+        dataStorageCfg.setSystemRegionMaxSize(DFLT_DATA_REGION_INITIAL_SIZE);
+
+        dataStorageCfg.setWalMode(LOG_ONLY);
+        dataStorageCfg.setWalSegments(WAL_SEGMENTS);
+        dataStorageCfg.setWalSegmentSize(WAL_SEGMENT_SZ);
+
+        cfg.setDataStorageConfiguration(dataStorageCfg);
+
+        cfg.setClientMode(client);
+
+        return cfg;
+    }
+
+    /**
+     * Starts read and write from cache in background.
+     *
+     * @param services Distributed services on the grid.
+     */
+    private static void deployServices(IgniteServices services) {
+        services.deployMultiple("Demo service: Multiple instances", new DemoServiceMultipleInstances(), 7, 3);
+        services.deployNodeSingleton("Demo service: Node singleton", new DemoServiceNodeSingleton());
+        services.deployClusterSingleton("Demo service: Cluster singleton", new DemoServiceClusterSingleton());
+        services.deployClusterSingleton("Demo caches load service", new DemoCachesLoadService(20));
+        services.deployKeyAffinitySingleton("Demo service: Key affinity singleton",
+            new DemoServiceKeyAffinity(), DemoCachesLoadService.CAR_CACHE_NAME, "id");
+
+        services.deployNodeSingleton("RandomCache load service", new DemoRandomCacheLoadService(20));
+
+        services.deployMultiple("Demo service: Compute load", new DemoComputeLoadService(), 2, 1);
+    }
+
+    /** */
+    public static List<String> getDemoUrl() {
+        return demoUrl;
+    }
+
+    /**
+     * Start ignite node with cacheEmployee and populate it with data.
+     */
+    public static CountDownLatch tryStart() {
+        if (initGuard.compareAndSet(false, true)) {
+            log.info("DEMO: Starting embedded nodes for demo...");
+
+            System.setProperty(IGNITE_NO_ASCII, "true");
+            System.setProperty(IGNITE_QUIET, "false");
+            System.setProperty(IGNITE_UPDATE_NOTIFIER, "false");
+
+            System.setProperty(IGNITE_ATOMIC_CACHE_DELETE_HISTORY_SIZE, "20");
+            System.setProperty(IGNITE_PERFORMANCE_SUGGESTIONS_DISABLED, "true");
+
+            final AtomicInteger basePort = new AtomicInteger(60700);
+            final AtomicInteger cnt = new AtomicInteger(-1);
+
+            final ScheduledExecutorService execSrv = newScheduledThreadPool(1, "demo-nodes-start");
+
+            execSrv.scheduleAtFixedRate(new Runnable() {
+                @Override public void run() {
+                    int idx = cnt.incrementAndGet();
+                    int port = basePort.get();
+
+                    boolean first = idx == 0;
+
+                    try {
+                        IgniteConfiguration cfg = igniteConfiguration(port, idx, false);
+
+                        if (first) {
+                            U.delete(Paths.get(cfg.getWorkDirectory()));
+
+                            U.resolveWorkDirectory(
+                                cfg.getWorkDirectory(),
+                                cfg.getDataStorageConfiguration().getStoragePath(),
+                                true
+                            );
+                        }
+
+                        Ignite ignite = Ignition.start(cfg);
+
+                        if (first) {
+                            ClusterNode node = ignite.cluster().localNode();
+
+                            Collection<String> jettyAddrs = node.attribute(ATTR_REST_JETTY_ADDRS);
+
+                            if (jettyAddrs == null) {
+                                Ignition.stopAll(true);
+
+                                throw new IgniteException("DEMO: Failed to start Jetty REST server on embedded node");
+                            }
+
+                            String jettyHost = jettyAddrs.iterator().next();
+
+                            Integer jettyPort = node.attribute(ATTR_REST_JETTY_PORT);
+
+                            if (F.isEmpty(jettyHost) || jettyPort == null)
+                                throw new IgniteException("DEMO: Failed to start Jetty REST handler on embedded node");
+
+                            log.info("DEMO: Started embedded node for demo purpose [TCP binary port={}, Jetty REST port={}]", port, jettyPort);
+
+                            demoUrl = Collections.singletonList(String.format("http://%s:%d", jettyHost, jettyPort));
+
+                            initLatch.countDown();
+                        }
+                    }
+                    catch (Throwable e) {
+                        if (first) {
+                            basePort.getAndAdd(50);
+
+                            log.warn("DEMO: Failed to start embedded node.", e);
+                        }
+                        else
+                            log.error("DEMO: Failed to start embedded node.", e);
+                    }
+                    finally {
+                        if (idx == NODE_CNT) {
+                            Ignite ignite = Ignition.ignite(SRV_NODE_NAME + 0);
+
+                            if (ignite != null) {
+                                ignite.cluster().active(true);
+
+                                deployServices(ignite.services(ignite.cluster().forServers()));
+                            }
+
+                            log.info("DEMO: All embedded nodes for demo successfully started");
+
+                            execSrv.shutdown();
+                        }
+                    }
+                }
+            }, 1, 5, TimeUnit.SECONDS);
+        }
+
+        return initLatch;
+    }
+
+    /** */
+    public static void stop() {
+        demoUrl = null;
+
+        Ignition.stopAll(true);
+
+        initLatch = new CountDownLatch(1);
+
+        initGuard.compareAndSet(true, false);
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/AgentDemoUtils.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/AgentDemoUtils.java
new file mode 100644
index 0000000..fb5edb2
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/AgentDemoUtils.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Utilities for Agent demo mode.
+ */
+public class AgentDemoUtils {
+    /** Counter for threads in pool. */
+    private static final AtomicInteger THREAD_CNT = new AtomicInteger(0);
+
+    /**
+     * Creates a thread pool that can schedule commands to run after a given delay, or to execute periodically.
+     *
+     * @param corePoolSize Number of threads to keep in the pool, even if they are idle.
+     * @param threadName Part of thread name that would be used by thread factory.
+     * @return Newly created scheduled thread pool.
+     */
+    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, final String threadName) {
+        ScheduledExecutorService srvc = Executors.newScheduledThreadPool(corePoolSize, new ThreadFactory() {
+            @Override public Thread newThread(Runnable r) {
+                Thread thread = new Thread(r, String.format("%s-%d", threadName, THREAD_CNT.getAndIncrement()));
+
+                thread.setDaemon(true);
+
+                return thread;
+            }
+        });
+
+        ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor)srvc;
+
+        // Setting up shutdown policy.
+        executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
+        executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
+
+        return srvc;
+    }
+
+    /**
+     * Round value.
+     *
+     * @param val Value to round.
+     * @param places Numbers after point.
+     * @return Rounded value;
+     */
+    public static double round(double val, int places) {
+        if (places < 0)
+            throw new IllegalArgumentException();
+
+        long factor = (long)Math.pow(10, places);
+
+        val *= factor;
+
+        long tmp = Math.round(val);
+
+        return (double)tmp / factor;
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/AgentMetadataDemo.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/AgentMetadataDemo.java
new file mode 100644
index 0000000..b017fb0
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/AgentMetadataDemo.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.log4j.Logger;
+import org.h2.tools.RunScript;
+import org.h2.tools.Server;
+
+import static org.apache.ignite.console.agent.AgentUtils.resolvePath;
+
+/**
+ * Demo for metadata load from database.
+ *
+ * H2 database will be started and several tables will be created.
+ */
+public class AgentMetadataDemo {
+    /** */
+    private static final Logger log = Logger.getLogger(AgentMetadataDemo.class.getName());
+
+    /** */
+    private static final AtomicBoolean initLatch = new AtomicBoolean();
+
+    /**
+     * @param jdbcUrl Connection url.
+     * @return true if url is used for test-drive.
+     */
+    public static boolean isTestDriveUrl(String jdbcUrl) {
+        return "jdbc:h2:mem:demo-db".equals(jdbcUrl);
+    }
+
+    /**
+     * Start H2 database and populate it with several tables.
+     */
+    public static Connection testDrive() throws SQLException {
+        if (initLatch.compareAndSet(false, true)) {
+            log.info("DEMO: Prepare in-memory H2 database...");
+
+            try {
+                Class.forName("org.h2.Driver");
+
+                Connection conn = DriverManager.getConnection("jdbc:h2:mem:demo-db;DB_CLOSE_DELAY=-1", "sa", "");
+
+                File sqlScript = resolvePath("demo/db-init.sql");
+
+                //noinspection ConstantConditions
+                RunScript.execute(conn, new FileReader(sqlScript));
+
+                log.info("DEMO: Sample tables created.");
+
+                conn.close();
+
+                Server.createTcpServer("-tcpDaemon").start();
+
+                log.info("DEMO: TcpServer stared.");
+
+                log.info("DEMO: JDBC URL for test drive metadata load: jdbc:h2:mem:demo-db");
+            }
+            catch (ClassNotFoundException e) {
+                log.error("DEMO: Failed to load H2 driver!", e);
+
+                throw new SQLException("Failed to load H2 driver", e);
+            }
+            catch (SQLException e) {
+                log.error("DEMO: Failed to start test drive for metadata!", e);
+
+                throw e;
+            }
+            catch (FileNotFoundException | NullPointerException e) {
+                log.error("DEMO: Failed to find demo database init script file: demo/db-init.sql");
+
+                throw new SQLException("Failed to start demo for metadata", e);
+            }
+        }
+
+        return DriverManager.getConnection("jdbc:h2:mem:demo-db;DB_CLOSE_DELAY=-1", "sa", "");
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Car.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Car.java
new file mode 100644
index 0000000..f351efc
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Car.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo.model;
+
+import java.io.Serializable;
+
+/**
+ * Car definition.
+ */
+public class Car implements Serializable {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Value for id. */
+    private int id;
+
+    /** Value for parkingId. */
+    private int parkingId;
+
+    /** Value for name. */
+    private String name;
+
+    /**
+     * Empty constructor.
+     */
+    public Car() {
+        // No-op.
+    }
+
+    /**
+     * Full constructor.
+     */
+    public Car(
+        int id,
+        int parkingId,
+        String name
+    ) {
+        this.id = id;
+        this.parkingId = parkingId;
+        this.name = name;
+    }
+
+    /**
+     * Gets id.
+     *
+     * @return Value for id.
+     */
+    public int getId() {
+        return id;
+    }
+
+    /**
+     * Sets id.
+     *
+     * @param id New value for id.
+     */
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    /**
+     * Gets parkingId.
+     *
+     * @return Value for parkingId.
+     */
+    public int getParkingId() {
+        return parkingId;
+    }
+
+    /**
+     * Sets parkingId.
+     *
+     * @param parkingId New value for parkingId.
+     */
+    public void setParkingId(int parkingId) {
+        this.parkingId = parkingId;
+    }
+
+    /**
+     * Gets name.
+     *
+     * @return Value for name.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets name.
+     *
+     * @param name New value for name.
+     */
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        
+        if (!(o instanceof Car))
+            return false;
+
+        Car that = (Car)o;
+
+        if (id != that.id)
+            return false;
+
+        if (parkingId != that.parkingId)
+            return false;
+
+        if (name != null ? !name.equals(that.name) : that.name != null)
+            return false;
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        int res = id;
+
+        res = 31 * res + parkingId;
+
+        res = 31 * res + (name != null ? name.hashCode() : 0);
+
+        return res;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "Car [id=" + id +
+            ", parkingId=" + parkingId +
+            ", name=" + name +
+            ']';
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Country.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Country.java
new file mode 100644
index 0000000..348928b
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Country.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo.model;
+
+import java.io.Serializable;
+
+/**
+ * Country definition.
+ */
+public class Country implements Serializable {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Value for id. */
+    private int id;
+
+    /** Value for name. */
+    private String name;
+
+    /** Value for population. */
+    private int population;
+
+    /**
+     * Empty constructor.
+     */
+    public Country() {
+        // No-op.
+    }
+
+    /**
+     * Full constructor.
+     */
+    public Country(
+        int id,
+        String name,
+        int population
+    ) {
+        this.id = id;
+        this.name = name;
+        this.population = population;
+    }
+
+    /**
+     * Gets id.
+     *
+     * @return Value for id.
+     */
+    public int getId() {
+        return id;
+    }
+
+    /**
+     * Sets id.
+     *
+     * @param id New value for id.
+     */
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    /**
+     * Gets name.
+     *
+     * @return Value for name.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets name.
+     *
+     * @param name New value for name.
+     */
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Gets population.
+     *
+     * @return Value for population.
+     */
+    public int getPopulation() {
+        return population;
+    }
+
+    /**
+     * Sets population.
+     *
+     * @param population New value for population.
+     */
+    public void setPopulation(int population) {
+        this.population = population;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        
+        if (!(o instanceof Country))
+            return false;
+
+        Country that = (Country)o;
+
+        if (id != that.id)
+            return false;
+
+        if (name != null ? !name.equals(that.name) : that.name != null)
+            return false;
+
+        if (population != that.population)
+            return false;
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        int res = id;
+
+        res = 31 * res + (name != null ? name.hashCode() : 0);
+
+        res = 31 * res + population;
+
+        return res;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "Country [id=" + id +
+            ", name=" + name +
+            ", population=" + population +
+            ']';
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Department.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Department.java
new file mode 100644
index 0000000..1c2f3b2
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Department.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo.model;
+
+import java.io.Serializable;
+
+/**
+ * Department definition.
+ */
+public class Department implements Serializable {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Value for id. */
+    private int id;
+
+    /** Value for countryId. */
+    private int countryId;
+
+    /** Value for name. */
+    private String name;
+
+    /**
+     * Empty constructor.
+     */
+    public Department() {
+        // No-op.
+    }
+
+    /**
+     * Full constructor.
+     */
+    public Department(
+        int id,
+        int countryId,
+        String name
+    ) {
+        this.id = id;
+        this.countryId = countryId;
+        this.name = name;
+    }
+
+    /**
+     * Gets id.
+     *
+     * @return Value for id.
+     */
+    public int getId() {
+        return id;
+    }
+
+    /**
+     * Sets id.
+     *
+     * @param id New value for id.
+     */
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    /**
+     * Gets countryId.
+     *
+     * @return Value for countryId.
+     */
+    public int getCountryId() {
+        return countryId;
+    }
+
+    /**
+     * Sets countryId.
+     *
+     * @param countryId New value for countryId.
+     */
+    public void setCountryId(int countryId) {
+        this.countryId = countryId;
+    }
+
+    /**
+     * Gets name.
+     *
+     * @return Value for name.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets name.
+     *
+     * @param name New value for name.
+     */
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        
+        if (!(o instanceof Department))
+            return false;
+
+        Department that = (Department)o;
+
+        if (id != that.id)
+            return false;
+
+        if (countryId != that.countryId)
+            return false;
+
+        if (name != null ? !name.equals(that.name) : that.name != null)
+            return false;
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        int res = id;
+
+        res = 31 * res + countryId;
+
+        res = 31 * res + (name != null ? name.hashCode() : 0);
+
+        return res;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "Department [id=" + id +
+            ", countryId=" + countryId +
+            ", name=" + name +
+            ']';
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Employee.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Employee.java
new file mode 100644
index 0000000..a3e7eba
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Employee.java
@@ -0,0 +1,356 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo.model;
+
+import java.io.Serializable;
+import java.sql.Date;
+
+/**
+ * Employee definition.
+ */
+public class Employee implements Serializable {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Value for id. */
+    private int id;
+
+    /** Value for departmentId. */
+    private int departmentId;
+
+    /** Value for managerId. */
+    private Integer managerId;
+
+    /** Value for firstName. */
+    private String firstName;
+
+    /** Value for lastName. */
+    private String lastName;
+
+    /** Value for email. */
+    private String email;
+
+    /** Value for phoneNumber. */
+    private String phoneNumber;
+
+    /** Value for hireDate. */
+    private Date hireDate;
+
+    /** Value for job. */
+    private String job;
+
+    /** Value for salary. */
+    private Double salary;
+
+    /**
+     * Empty constructor.
+     */
+    public Employee() {
+        // No-op.
+    }
+
+    /**
+     * Full constructor.
+     */
+    public Employee(
+        int id,
+        int departmentId,
+        Integer managerId,
+        String firstName,
+        String lastName,
+        String email,
+        String phoneNumber,
+        Date hireDate,
+        String job,
+        Double salary
+    ) {
+        this.id = id;
+        this.departmentId = departmentId;
+        this.managerId = managerId;
+        this.firstName = firstName;
+        this.lastName = lastName;
+        this.email = email;
+        this.phoneNumber = phoneNumber;
+        this.hireDate = hireDate;
+        this.job = job;
+        this.salary = salary;
+    }
+
+    /**
+     * Gets id.
+     *
+     * @return Value for id.
+     */
+    public int getId() {
+        return id;
+    }
+
+    /**
+     * Sets id.
+     *
+     * @param id New value for id.
+     */
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    /**
+     * Gets departmentId.
+     *
+     * @return Value for departmentId.
+     */
+    public int getDepartmentId() {
+        return departmentId;
+    }
+
+    /**
+     * Sets departmentId.
+     *
+     * @param departmentId New value for departmentId.
+     */
+    public void setDepartmentId(int departmentId) {
+        this.departmentId = departmentId;
+    }
+
+    /**
+     * Gets managerId.
+     *
+     * @return Value for managerId.
+     */
+    public Integer getManagerId() {
+        return managerId;
+    }
+
+    /**
+     * Sets managerId.
+     *
+     * @param managerId New value for managerId.
+     */
+    public void setManagerId(Integer managerId) {
+        this.managerId = managerId;
+    }
+
+    /**
+     * Gets firstName.
+     *
+     * @return Value for firstName.
+     */
+    public String getFirstName() {
+        return firstName;
+    }
+
+    /**
+     * Sets firstName.
+     *
+     * @param firstName New value for firstName.
+     */
+    public void setFirstName(String firstName) {
+        this.firstName = firstName;
+    }
+
+    /**
+     * Gets lastName.
+     *
+     * @return Value for lastName.
+     */
+    public String getLastName() {
+        return lastName;
+    }
+
+    /**
+     * Sets lastName.
+     *
+     * @param lastName New value for lastName.
+     */
+    public void setLastName(String lastName) {
+        this.lastName = lastName;
+    }
+
+    /**
+     * Gets email.
+     *
+     * @return Value for email.
+     */
+    public String getEmail() {
+        return email;
+    }
+
+    /**
+     * Sets email.
+     *
+     * @param email New value for email.
+     */
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    /**
+     * Gets phoneNumber.
+     *
+     * @return Value for phoneNumber.
+     */
+    public String getPhoneNumber() {
+        return phoneNumber;
+    }
+
+    /**
+     * Sets phoneNumber.
+     *
+     * @param phoneNumber New value for phoneNumber.
+     */
+    public void setPhoneNumber(String phoneNumber) {
+        this.phoneNumber = phoneNumber;
+    }
+
+    /**
+     * Gets hireDate.
+     *
+     * @return Value for hireDate.
+     */
+    public Date getHireDate() {
+        return hireDate;
+    }
+
+    /**
+     * Sets hireDate.
+     *
+     * @param hireDate New value for hireDate.
+     */
+    public void setHireDate(Date hireDate) {
+        this.hireDate = hireDate;
+    }
+
+    /**
+     * Gets job.
+     *
+     * @return Value for job.
+     */
+    public String getJob() {
+        return job;
+    }
+
+    /**
+     * Sets job.
+     *
+     * @param job New value for job.
+     */
+    public void setJob(String job) {
+        this.job = job;
+    }
+
+    /**
+     * Gets salary.
+     *
+     * @return Value for salary.
+     */
+    public Double getSalary() {
+        return salary;
+    }
+
+    /**
+     * Sets salary.
+     *
+     * @param salary New value for salary.
+     */
+    public void setSalary(Double salary) {
+        this.salary = salary;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        
+        if (!(o instanceof Employee))
+            return false;
+
+        Employee that = (Employee)o;
+
+        if (id != that.id)
+            return false;
+
+        if (departmentId != that.departmentId)
+            return false;
+
+        if (managerId != null ? !managerId.equals(that.managerId) : that.managerId != null)
+            return false;
+
+        if (firstName != null ? !firstName.equals(that.firstName) : that.firstName != null)
+            return false;
+
+        if (lastName != null ? !lastName.equals(that.lastName) : that.lastName != null)
+            return false;
+
+        if (email != null ? !email.equals(that.email) : that.email != null)
+            return false;
+
+        if (phoneNumber != null ? !phoneNumber.equals(that.phoneNumber) : that.phoneNumber != null)
+            return false;
+
+        if (hireDate != null ? !hireDate.equals(that.hireDate) : that.hireDate != null)
+            return false;
+
+        if (job != null ? !job.equals(that.job) : that.job != null)
+            return false;
+
+        if (salary != null ? !salary.equals(that.salary) : that.salary != null)
+            return false;
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        int res = id;
+
+        res = 31 * res + departmentId;
+
+        res = 31 * res + (managerId != null ? managerId.hashCode() : 0);
+
+        res = 31 * res + (firstName != null ? firstName.hashCode() : 0);
+
+        res = 31 * res + (lastName != null ? lastName.hashCode() : 0);
+
+        res = 31 * res + (email != null ? email.hashCode() : 0);
+
+        res = 31 * res + (phoneNumber != null ? phoneNumber.hashCode() : 0);
+
+        res = 31 * res + (hireDate != null ? hireDate.hashCode() : 0);
+
+        res = 31 * res + (job != null ? job.hashCode() : 0);
+
+        res = 31 * res + (salary != null ? salary.hashCode() : 0);
+
+        return res;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "Employee [id=" + id +
+            ", departmentId=" + departmentId +
+            ", managerId=" + managerId +
+            ", firstName=" + firstName +
+            ", lastName=" + lastName +
+            ", email=" + email +
+            ", phoneNumber=" + phoneNumber +
+            ", hireDate=" + hireDate +
+            ", job=" + job +
+            ", salary=" + salary +
+            ']';
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Parking.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Parking.java
new file mode 100644
index 0000000..d55ae81
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/model/Parking.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo.model;
+
+import java.io.Serializable;
+
+/**
+ * Parking definition.
+ */
+public class Parking implements Serializable {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Value for id. */
+    private int id;
+
+    /** Value for name. */
+    private String name;
+
+    /** Value for capacity. */
+    private int capacity;
+
+    /**
+     * Empty constructor.
+     */
+    public Parking() {
+        // No-op.
+    }
+
+    /**
+     * Full constructor.
+     */
+    public Parking(
+        int id,
+        String name,
+        int capacity
+    ) {
+        this.id = id;
+        this.name = name;
+        this.capacity = capacity;
+    }
+
+    /**
+     * Gets id.
+     *
+     * @return Value for id.
+     */
+    public int getId() {
+        return id;
+    }
+
+    /**
+     * Sets id.
+     *
+     * @param id New value for id.
+     */
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    /**
+     * Gets name.
+     *
+     * @return Value for name.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets name.
+     *
+     * @param name New value for name.
+     */
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Gets capacity.
+     *
+     * @return Value for capacity.
+     */
+    public int getCapacity() {
+        return capacity;
+    }
+
+    /**
+     * Sets capacity.
+     *
+     * @param capacity New value for capacity.
+     */
+    public void setCapacity(int capacity) {
+        this.capacity = capacity;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        
+        if (!(o instanceof Parking))
+            return false;
+
+        Parking that = (Parking)o;
+
+        if (id != that.id)
+            return false;
+
+        if (name != null ? !name.equals(that.name) : that.name != null)
+            return false;
+
+        if (capacity != that.capacity)
+            return false;
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        int res = id;
+
+        res = 31 * res + (name != null ? name.hashCode() : 0);
+
+        res = 31 * res + capacity;
+
+        return res;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "Parking [id=" + id +
+            ", name=" + name +
+            ", capacity=" + capacity +
+            ']';
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoCachesLoadService.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoCachesLoadService.java
new file mode 100644
index 0000000..774715c
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoCachesLoadService.java
@@ -0,0 +1,498 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo.service;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.QueryEntity;
+import org.apache.ignite.cache.QueryIndex;
+import org.apache.ignite.cache.QueryIndexType;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.cache.query.annotations.QuerySqlFunction;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.console.demo.AgentDemoUtils;
+import org.apache.ignite.console.demo.model.Car;
+import org.apache.ignite.console.demo.model.Country;
+import org.apache.ignite.console.demo.model.Department;
+import org.apache.ignite.console.demo.model.Employee;
+import org.apache.ignite.console.demo.model.Parking;
+import org.apache.ignite.resources.IgniteInstanceResource;
+import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceContext;
+import org.apache.ignite.transactions.Transaction;
+
+import static org.apache.ignite.transactions.TransactionConcurrency.PESSIMISTIC;
+import static org.apache.ignite.transactions.TransactionIsolation.REPEATABLE_READ;
+
+/**
+ * Demo service. Create and populate caches. Run demo load on caches.
+ */
+public class DemoCachesLoadService implements Service {
+    /** Ignite instance. */
+    @IgniteInstanceResource
+    private Ignite ignite;
+
+    /** Thread pool to execute cache load operations. */
+    private ScheduledExecutorService cachePool;
+
+    /** */
+    private static final String COUNTRY_CACHE_NAME = "CountryCache";
+
+    /** */
+    private static final String DEPARTMENT_CACHE_NAME = "DepartmentCache";
+
+    /** */
+    private static final String EMPLOYEE_CACHE_NAME = "EmployeeCache";
+
+    /** */
+    private static final String PARKING_CACHE_NAME = "ParkingCache";
+
+    /** */
+    public static final String CAR_CACHE_NAME = "CarCache";
+
+    /** */
+    static final Set<String> DEMO_CACHES = new HashSet<>(Arrays.asList(COUNTRY_CACHE_NAME,
+        DEPARTMENT_CACHE_NAME, EMPLOYEE_CACHE_NAME, PARKING_CACHE_NAME, CAR_CACHE_NAME));
+
+    /** Countries count. */
+    private static final int CNTR_CNT = 10;
+
+    /** Departments count */
+    private static final int DEP_CNT = 100;
+
+    /** Employees count. */
+    private static final int EMPL_CNT = 1000;
+
+    /** Countries count. */
+    private static final int CAR_CNT = 100;
+
+    /** Departments count */
+    private static final int PARK_CNT = 10;
+
+    /** */
+    private static final Random rnd = new Random();
+
+    /** Maximum count read/write key. */
+    private final int cnt;
+
+    /** Time range in milliseconds. */
+    private final long range;
+
+    /**
+     * @param cnt Maximum count read/write key.
+     */
+    public DemoCachesLoadService(int cnt) {
+        this.cnt = cnt;
+
+        range = new java.util.Date().getTime();
+    }
+
+    /** {@inheritDoc} */
+    @Override public void cancel(ServiceContext ctx) {
+        if (cachePool != null)
+            cachePool.shutdownNow();
+    }
+
+    /** {@inheritDoc} */
+    @Override public void init(ServiceContext ctx) throws Exception {
+        ignite.getOrCreateCaches(Arrays.asList(
+            cacheCountry(), cacheDepartment(), cacheEmployee(), cacheCar(), cacheParking()
+        ));
+
+        populateCacheEmployee();
+        populateCacheCar();
+
+        cachePool = AgentDemoUtils.newScheduledThreadPool(2, "demo-sql-load-cache-tasks");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void execute(ServiceContext ctx) throws Exception {
+        cachePool.scheduleWithFixedDelay(new Runnable() {
+            @Override public void run() {
+                try {
+                    IgniteCache<Integer, Employee> cacheEmployee = ignite.cache(EMPLOYEE_CACHE_NAME);
+
+                    if (cacheEmployee != null)
+                        try (Transaction tx = ignite.transactions().txStart(PESSIMISTIC, REPEATABLE_READ)) {
+                            for (int i = 0, n = 1; i < cnt; i++, n++) {
+                                Integer id = rnd.nextInt(EMPL_CNT);
+
+                                Integer depId = rnd.nextInt(DEP_CNT);
+
+                                double r = rnd.nextDouble();
+
+                                cacheEmployee.put(id, new Employee(id, depId, depId, "First name employee #" + n,
+                                    "Last name employee #" + n, "Email employee #" + n, "Phone number employee #" + n,
+                                    new java.sql.Date((long)(r * range)), "Job employee #" + n,
+                                    500 + AgentDemoUtils.round(r * 2000, 2)));
+
+                                if (rnd.nextBoolean())
+                                    cacheEmployee.remove(rnd.nextInt(EMPL_CNT));
+
+                                cacheEmployee.get(rnd.nextInt(EMPL_CNT));
+                            }
+
+                            if (rnd.nextInt(100) > 20)
+                                tx.commit();
+                        }
+                }
+                catch (Throwable e) {
+                    if (!e.getMessage().contains("cache is stopped"))
+                        ignite.log().error("Cache write task execution error", e);
+                }
+            }
+        }, 10, 3, TimeUnit.SECONDS);
+
+        cachePool.scheduleWithFixedDelay(new Runnable() {
+            @Override public void run() {
+                try {
+                    IgniteCache<Integer, Car> cache = ignite.cache(CAR_CACHE_NAME);
+
+                    if (cache != null)
+                        for (int i = 0; i < cnt; i++) {
+                            Integer carId = rnd.nextInt(CAR_CNT);
+
+                            cache.put(carId, new Car(carId, rnd.nextInt(PARK_CNT), "Car #" + (i + 1)));
+
+                            if (rnd.nextBoolean())
+                                cache.remove(rnd.nextInt(CAR_CNT));
+                        }
+                }
+                catch (IllegalStateException ignored) {
+                    // No-op.
+                }
+                catch (Throwable e) {
+                    if (!e.getMessage().contains("cache is stopped"))
+                        ignite.log().error("Cache write task execution error", e);
+                }
+            }
+        }, 10, 3, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Create base cache configuration.
+     *
+     * @param name cache name.
+     * @return Cache configuration with basic properties set.
+     */
+    private static CacheConfiguration cacheConfiguration(String name) {
+        CacheConfiguration ccfg = new CacheConfiguration<>(name);
+
+        ccfg.setAffinity(new RendezvousAffinityFunction(false, 32));
+        ccfg.setQueryDetailMetricsSize(10);
+        ccfg.setStatisticsEnabled(true);
+        ccfg.setSqlFunctionClasses(SQLFunctions.class);
+        ccfg.setDataRegionName("demo");
+
+        return ccfg;
+    }
+
+    /**
+     * Configure cacheCountry.
+     */
+    private static CacheConfiguration cacheCountry() {
+        CacheConfiguration ccfg = cacheConfiguration(COUNTRY_CACHE_NAME);
+
+        // Configure cacheCountry types.
+        Collection<QueryEntity> qryEntities = new ArrayList<>();
+
+        // COUNTRY.
+        QueryEntity type = new QueryEntity();
+
+        qryEntities.add(type);
+
+        type.setKeyType(Integer.class.getName());
+        type.setValueType(Country.class.getName());
+
+        // Query fields for COUNTRY.
+        LinkedHashMap<String, String> qryFlds = new LinkedHashMap<>();
+
+        qryFlds.put("id", "java.lang.Integer");
+        qryFlds.put("name", "java.lang.String");
+        qryFlds.put("population", "java.lang.Integer");
+
+        type.setFields(qryFlds);
+
+        ccfg.setQueryEntities(qryEntities);
+
+        return ccfg;
+    }
+
+    /**
+     * Configure cacheEmployee.
+     */
+    private static CacheConfiguration cacheDepartment() {
+        CacheConfiguration ccfg = cacheConfiguration(DEPARTMENT_CACHE_NAME);
+
+        // Configure cacheDepartment types.
+        Collection<QueryEntity> qryEntities = new ArrayList<>();
+
+        // DEPARTMENT.
+        QueryEntity type = new QueryEntity();
+
+        qryEntities.add(type);
+
+        type.setKeyType(Integer.class.getName());
+        type.setValueType(Department.class.getName());
+
+        // Query fields for DEPARTMENT.
+        LinkedHashMap<String, String> qryFlds = new LinkedHashMap<>();
+
+        qryFlds.put("id", "java.lang.Integer");
+        qryFlds.put("countryId", "java.lang.Integer");
+        qryFlds.put("name", "java.lang.String");
+
+        type.setFields(qryFlds);
+
+        // Indexes for DEPARTMENT.
+
+        ArrayList<QueryIndex> indexes = new ArrayList<>();
+
+        indexes.add(new QueryIndex("countryId", QueryIndexType.SORTED, false, "DEP_COUNTRY"));
+
+        type.setIndexes(indexes);
+
+        ccfg.setQueryEntities(qryEntities);
+
+        return ccfg;
+    }
+
+    /**
+     * Configure cacheEmployee.
+     */
+    private static CacheConfiguration cacheEmployee() {
+        CacheConfiguration ccfg = cacheConfiguration(EMPLOYEE_CACHE_NAME);
+
+        ccfg.setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL);
+        ccfg.setBackups(1);
+
+        // Configure cacheEmployee types.
+        Collection<QueryEntity> qryEntities = new ArrayList<>();
+
+        // EMPLOYEE.
+        QueryEntity type = new QueryEntity();
+
+        qryEntities.add(type);
+
+        type.setKeyType(Integer.class.getName());
+        type.setValueType(Employee.class.getName());
+
+        // Query fields for EMPLOYEE.
+        LinkedHashMap<String, String> qryFlds = new LinkedHashMap<>();
+
+        qryFlds.put("id", "java.lang.Integer");
+        qryFlds.put("departmentId", "java.lang.Integer");
+        qryFlds.put("managerId", "java.lang.Integer");
+        qryFlds.put("firstName", "java.lang.String");
+        qryFlds.put("lastName", "java.lang.String");
+        qryFlds.put("email", "java.lang.String");
+        qryFlds.put("phoneNumber", "java.lang.String");
+        qryFlds.put("hireDate", "java.sql.Date");
+        qryFlds.put("job", "java.lang.String");
+        qryFlds.put("salary", "java.lang.Double");
+
+        type.setFields(qryFlds);
+
+        // Indexes for EMPLOYEE.
+
+        Collection<QueryIndex> indexes = new ArrayList<>();
+
+        indexes.add(new QueryIndex("departmentId", QueryIndexType.SORTED, false, "EMP_DEPARTMENT"));
+        indexes.add(new QueryIndex("managerId", QueryIndexType.SORTED, false, "EMP_MANAGER"));
+
+        QueryIndex idx = new QueryIndex();
+
+        idx.setName("EMP_NAMES");
+        idx.setIndexType(QueryIndexType.SORTED);
+        LinkedHashMap<String, Boolean> indFlds = new LinkedHashMap<>();
+
+        indFlds.put("firstName", Boolean.FALSE);
+        indFlds.put("lastName", Boolean.FALSE);
+
+        idx.setFields(indFlds);
+
+        indexes.add(idx);
+        indexes.add(new QueryIndex("salary", QueryIndexType.SORTED, false, "EMP_SALARY"));
+
+        type.setIndexes(indexes);
+
+        ccfg.setQueryEntities(qryEntities);
+
+        return ccfg;
+    }
+
+    /**
+     * Configure cacheEmployee.
+     */
+    private static CacheConfiguration cacheParking() {
+        CacheConfiguration ccfg = cacheConfiguration(PARKING_CACHE_NAME);
+
+        // Configure cacheParking types.
+        Collection<QueryEntity> qryEntities = new ArrayList<>();
+
+        // PARKING.
+        QueryEntity type = new QueryEntity();
+
+        qryEntities.add(type);
+
+        type.setKeyType(Integer.class.getName());
+        type.setValueType(Parking.class.getName());
+
+        // Query fields for PARKING.
+        LinkedHashMap<String, String> qryFlds = new LinkedHashMap<>();
+
+        qryFlds.put("id", "java.lang.Integer");
+        qryFlds.put("name", "java.lang.String");
+        qryFlds.put("capacity", "java.lang.Integer");
+
+        type.setFields(qryFlds);
+
+        ccfg.setQueryEntities(qryEntities);
+
+        return ccfg;
+    }
+
+    /**
+     * Configure cacheEmployee.
+     */
+    private static CacheConfiguration cacheCar() {
+        CacheConfiguration ccfg = cacheConfiguration(CAR_CACHE_NAME);
+
+        // Configure cacheCar types.
+        Collection<QueryEntity> qryEntities = new ArrayList<>();
+
+        // CAR.
+        QueryEntity type = new QueryEntity();
+
+        qryEntities.add(type);
+
+        type.setKeyType(Integer.class.getName());
+        type.setValueType(Car.class.getName());
+
+        // Query fields for CAR.
+        LinkedHashMap<String, String> qryFlds = new LinkedHashMap<>();
+
+        qryFlds.put("id", "java.lang.Integer");
+        qryFlds.put("parkingId", "java.lang.Integer");
+        qryFlds.put("name", "java.lang.String");
+
+        type.setFields(qryFlds);
+
+        // Indexes for CAR.
+
+        ArrayList<QueryIndex> indexes = new ArrayList<>();
+
+        indexes.add(new QueryIndex("parkingId", QueryIndexType.SORTED, false, "CAR_PARKING"));
+        type.setIndexes(indexes);
+
+        ccfg.setQueryEntities(qryEntities);
+
+        return ccfg;
+    }
+
+    /** */
+    private void populateCacheEmployee() {
+        if (ignite.log().isDebugEnabled())
+            ignite.log().debug("DEMO: Start employees population with data...");
+
+        IgniteCache<Integer, Country> cacheCountry = ignite.cache(COUNTRY_CACHE_NAME);
+
+        for (int i = 0, n = 1; i < CNTR_CNT; i++, n++)
+            cacheCountry.put(i, new Country(i, "Country #" + n, n * 10000000));
+
+        IgniteCache<Integer, Department> cacheDepartment = ignite.cache(DEPARTMENT_CACHE_NAME);
+
+        IgniteCache<Integer, Employee> cacheEmployee = ignite.cache(EMPLOYEE_CACHE_NAME);
+
+        for (int i = 0, n = 1; i < DEP_CNT; i++, n++) {
+            cacheDepartment.put(i, new Department(n, rnd.nextInt(CNTR_CNT), "Department #" + n));
+
+            double r = rnd.nextDouble();
+
+            cacheEmployee.put(i, new Employee(i, rnd.nextInt(DEP_CNT), null, "First name manager #" + n,
+                "Last name manager #" + n, "Email manager #" + n, "Phone number manager #" + n,
+                new java.sql.Date((long)(r * range)), "Job manager #" + n, 1000 + AgentDemoUtils.round(r * 4000, 2)));
+        }
+
+        for (int i = 0, n = 1; i < EMPL_CNT; i++, n++) {
+            Integer depId = rnd.nextInt(DEP_CNT);
+
+            double r = rnd.nextDouble();
+
+            cacheEmployee.put(i, new Employee(i, depId, depId, "First name employee #" + n,
+                "Last name employee #" + n, "Email employee #" + n, "Phone number employee #" + n,
+                new java.sql.Date((long)(r * range)), "Job employee #" + n, 500 + AgentDemoUtils.round(r * 2000, 2)));
+        }
+
+        if (ignite.log().isDebugEnabled())
+            ignite.log().debug("DEMO: Finished employees population.");
+    }
+
+    /** */
+    private void populateCacheCar() {
+        if (ignite.log().isDebugEnabled())
+            ignite.log().debug("DEMO: Start cars population...");
+
+        IgniteCache<Integer, Parking> cacheParking = ignite.cache(PARKING_CACHE_NAME);
+
+        for (int i = 0, n = 1; i < PARK_CNT; i++, n++)
+            cacheParking.put(i, new Parking(i, "Parking #" + n, n * 10));
+
+        IgniteCache<Integer, Car> cacheCar = ignite.cache(CAR_CACHE_NAME);
+
+        for (int i = 0, n = 1; i < CAR_CNT; i++, n++)
+            cacheCar.put(i, new Car(i, rnd.nextInt(PARK_CNT), "Car #" + n));
+
+        if (ignite.log().isDebugEnabled())
+            ignite.log().debug("DEMO: Finished cars population.");
+    }
+
+    /**
+     * Utility class with custom SQL functions.
+     */
+    public static class SQLFunctions {
+        /**
+         * Sleep function to simulate long running queries.
+         *
+         * @param x Time to sleep.
+         * @return Return specified argument.
+         */
+        @QuerySqlFunction
+        public static long sleep(long x) {
+            if (x >= 0)
+                try {
+                    Thread.sleep(x);
+                }
+                catch (InterruptedException ignored) {
+                    // No-op.
+                }
+
+            return x;
+        }
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoComputeLoadService.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoComputeLoadService.java
new file mode 100644
index 0000000..3c767c8
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoComputeLoadService.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo.service;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.console.demo.AgentDemoUtils;
+import org.apache.ignite.console.demo.task.DemoCancellableTask;
+import org.apache.ignite.console.demo.task.DemoComputeTask;
+import org.apache.ignite.resources.IgniteInstanceResource;
+import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceContext;
+
+/**
+ * Demo service. Run tasks on nodes. Run demo load on caches.
+ */
+public class DemoComputeLoadService implements Service {
+    /** Ignite instance. */
+    @IgniteInstanceResource
+    private Ignite ignite;
+
+    /** Thread pool to execute cache load operations. */
+    private ScheduledExecutorService computePool;
+
+    /** {@inheritDoc} */
+    @Override public void cancel(ServiceContext ctx) {
+        if (computePool != null)
+            computePool.shutdownNow();
+    }
+
+    /** {@inheritDoc} */
+    @Override public void init(ServiceContext ctx) throws Exception {
+        computePool = AgentDemoUtils.newScheduledThreadPool(2, "demo-compute-load-tasks");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void execute(ServiceContext ctx) throws Exception {
+        computePool.scheduleWithFixedDelay(new Runnable() {
+            @Override public void run() {
+                try {
+                    ignite.compute().withNoFailover()
+                        .execute(DemoComputeTask.class, null);
+                }
+                catch (Throwable e) {
+                    ignite.log().error("Task execution error", e);
+                }
+            }
+        }, 10, 3, TimeUnit.SECONDS);
+
+        computePool.scheduleWithFixedDelay(new Runnable() {
+            @Override public void run() {
+                try {
+                    ignite.compute().withNoFailover()
+                        .execute(DemoCancellableTask.class, null);
+                }
+                catch (Throwable e) {
+                    ignite.log().error("DemoCancellableTask execution error", e);
+                }
+            }
+        }, 10, 30, TimeUnit.SECONDS);
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoRandomCacheLoadService.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoRandomCacheLoadService.java
new file mode 100644
index 0000000..5b73e96
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoRandomCacheLoadService.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo.service;
+
+import java.util.Random;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.console.demo.AgentDemoUtils;
+import org.apache.ignite.resources.IgniteInstanceResource;
+import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceContext;
+
+import static org.apache.ignite.internal.processors.query.QueryUtils.DFLT_SCHEMA;
+
+/**
+ * Demo service. Create cache and populate it by random int pairs.
+ */
+public class DemoRandomCacheLoadService implements Service {
+    /** Ignite instance. */
+    @IgniteInstanceResource
+    private Ignite ignite;
+
+    /** Thread pool to execute cache load operations. */
+    private ScheduledExecutorService cachePool;
+
+    /** */
+    public static final String RANDOM_CACHE_NAME = "RandomCache";
+
+    /** Employees count. */
+    private static final int RND_CNT = 1024;
+
+    /** */
+    private static final Random rnd = new Random();
+
+    /** Maximum count read/write key. */
+    private final int cnt;
+
+    /**
+     * @param cnt Maximum count read/write key.
+     */
+    public DemoRandomCacheLoadService(int cnt) {
+        this.cnt = cnt;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void cancel(ServiceContext ctx) {
+        if (cachePool != null)
+            cachePool.shutdownNow();
+    }
+
+    /** {@inheritDoc} */
+    @Override public void init(ServiceContext ctx) throws Exception {
+        ignite.getOrCreateCache(cacheRandom());
+
+        cachePool = AgentDemoUtils.newScheduledThreadPool(2, "demo-sql-random-load-cache-tasks");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void execute(ServiceContext ctx) throws Exception {
+        cachePool.scheduleWithFixedDelay(new Runnable() {
+            @Override public void run() {
+                try {
+                    for (String cacheName : ignite.cacheNames()) {
+                        IgniteCache<Integer, Integer> cache = ignite.cache(cacheName);
+
+                        if (cache != null &&
+                            !DemoCachesLoadService.DEMO_CACHES.contains(cacheName) &&
+                            !DFLT_SCHEMA.equalsIgnoreCase(cache.getConfiguration(CacheConfiguration.class).getSqlSchema())) {
+                            for (int i = 0, n = 1; i < cnt; i++, n++) {
+                                Integer key = rnd.nextInt(RND_CNT);
+                                Integer val = rnd.nextInt(RND_CNT);
+
+                                cache.put(key, val);
+
+                                if (rnd.nextInt(100) < 30)
+                                    cache.remove(key);
+                            }
+                        }
+                    }
+                }
+                catch (Throwable e) {
+                    if (!e.getMessage().contains("cache is stopped"))
+                        ignite.log().error("Cache write task execution error", e);
+                }
+            }
+        }, 10, 3, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Configure cacheCountry.
+     */
+    private static <K, V> CacheConfiguration<K, V> cacheRandom() {
+        CacheConfiguration<K, V> ccfg = new CacheConfiguration<>(RANDOM_CACHE_NAME);
+
+        ccfg.setAffinity(new RendezvousAffinityFunction(false, 32));
+        ccfg.setQueryDetailMetricsSize(10);
+        ccfg.setStatisticsEnabled(true);
+        ccfg.setIndexedTypes(Integer.class, Integer.class);
+
+        return ccfg;
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoServiceClusterSingleton.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoServiceClusterSingleton.java
new file mode 100644
index 0000000..8c0623a
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoServiceClusterSingleton.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo.service;
+
+import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceContext;
+
+/**
+ * Demo service to provide on one node in cluster.
+ */
+public class DemoServiceClusterSingleton implements Service {
+    /** {@inheritDoc} */
+    @Override public void cancel(ServiceContext ctx) {
+        // No-op.
+    }
+
+    /** {@inheritDoc} */
+    @Override public void init(ServiceContext ctx) throws Exception {
+        // No-op.
+    }
+
+    /** {@inheritDoc} */
+    @Override public void execute(ServiceContext ctx) throws Exception {
+        // No-op.
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoServiceKeyAffinity.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoServiceKeyAffinity.java
new file mode 100644
index 0000000..081ae27
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoServiceKeyAffinity.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo.service;
+
+import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceContext;
+
+/**
+ * Demo service to provide for cache.
+ */
+public class DemoServiceKeyAffinity implements Service {
+    /** {@inheritDoc} */
+    @Override public void cancel(ServiceContext ctx) {
+        // No-op.
+    }
+
+    /** {@inheritDoc} */
+    @Override public void init(ServiceContext ctx) throws Exception {
+        // No-op.
+    }
+
+    /** {@inheritDoc} */
+    @Override public void execute(ServiceContext ctx) throws Exception {
+        // No-op.
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoServiceMultipleInstances.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoServiceMultipleInstances.java
new file mode 100644
index 0000000..0d10753
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoServiceMultipleInstances.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo.service;
+
+import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceContext;
+
+/**
+ * Demo service to provide on all nodes.
+ */
+public class DemoServiceMultipleInstances implements Service {
+    /** {@inheritDoc} */
+    @Override public void cancel(ServiceContext ctx) {
+        // No-op.
+    }
+
+    /** {@inheritDoc} */
+    @Override public void init(ServiceContext ctx) throws Exception {
+        // No-op.
+    }
+
+    /** {@inheritDoc} */
+    @Override public void execute(ServiceContext ctx) throws Exception {
+        // No-op.
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoServiceNodeSingleton.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoServiceNodeSingleton.java
new file mode 100644
index 0000000..4d491da
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/service/DemoServiceNodeSingleton.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo.service;
+
+import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceContext;
+
+/**
+ * Demo service to provide on all nodes by one.
+ */
+public class DemoServiceNodeSingleton implements Service {
+    /** {@inheritDoc} */
+    @Override public void cancel(ServiceContext ctx) {
+        // No-op.
+    }
+
+    /** {@inheritDoc} */
+    @Override public void init(ServiceContext ctx) throws Exception {
+        // No-op.
+    }
+
+    /** {@inheritDoc} */
+    @Override public void execute(ServiceContext ctx) throws Exception {
+        // No-op.
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/task/DemoCancellableTask.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/task/DemoCancellableTask.java
new file mode 100644
index 0000000..1113275
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/task/DemoCancellableTask.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.console.demo.task;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.IgniteInterruptedException;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.compute.ComputeJob;
+import org.apache.ignite.compute.ComputeJobAdapter;
+import org.apache.ignite.compute.ComputeJobResult;
+import org.apache.ignite.compute.ComputeJobResultPolicy;
+import org.apache.ignite.compute.ComputeTask;
+import org.apache.ignite.internal.util.typedef.internal.S;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Simple compute task to test task cancellation from Visor.
+ */
+public class DemoCancellableTask implements ComputeTask<Void, Void> {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** {@inheritDoc} */
+    @NotNull @Override public Map<? extends ComputeJob, ClusterNode> map(List<ClusterNode> subgrid,
+        @Nullable Void arg) throws IgniteException {
+        HashMap<ComputeJob, ClusterNode> map = U.newHashMap(1);
+
+        map.put(new DemoCancellableJob(), subgrid.get(0));
+
+        return map;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ComputeJobResultPolicy result(ComputeJobResult res, List<ComputeJobResult> rcvd) throws IgniteException {
+        return ComputeJobResultPolicy.WAIT;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable @Override public Void reduce(List<ComputeJobResult> results) throws IgniteException {
+        return null;
+    }
+
+    /**
+     * Simple compute job to execute cancel action.
+     */
+    private static class DemoCancellableJob extends ComputeJobAdapter {
+        /** */
+        private static final long serialVersionUID = 0L;
+
+        /** Random generator. */
+        private static final Random rnd = new Random();
+
+        /** {@inheritDoc} */
+        @Override public Object execute() throws IgniteException {
+            try {
+                Thread.sleep(1000 + rnd.nextInt(60000));
+            }
+            catch (InterruptedException e) {
+                // Restore interrupt status
+                Thread.currentThread().interrupt();
+
+                throw new IgniteInterruptedException(e);
+            }
+
+            return null;
+        }
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(DemoCancellableJob.class, this);
+        }
+    }
+}
diff --git a/modules/web-agent/src/main/java/org/apache/ignite/console/demo/task/DemoComputeTask.java b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/task/DemoComputeTask.java
new file mode 100644
index 0000000..7e5b784
--- /dev/null
+++ b/modules/web-agent/src/main/java/org/apache/ignite/console/demo/task/DemoComputeTask.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.demo.task;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.compute.ComputeJob;
+import org.apache.ignite.compute.ComputeJobAdapter;
+import org.apache.ignite.compute.ComputeJobResult;
+import org.apache.ignite.compute.ComputeJobResultPolicy;
+import org.apache.ignite.compute.ComputeTask;
+import org.apache.ignite.internal.util.typedef.internal.S;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Simple compute task.
+ */
+public class DemoComputeTask implements ComputeTask<Void, Integer> {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Random generator. */
+    private static final Random rnd = new Random();
+
+    /** {@inheritDoc} */
+    @NotNull @Override public Map<? extends ComputeJob, ClusterNode> map(List<ClusterNode> subgrid,
+        @Nullable Void arg) throws IgniteException {
+        HashMap<ComputeJob, ClusterNode> map = new HashMap<>(subgrid.size());
+
+        for (ClusterNode node: subgrid) {
+            for (int i = 0; i < Math.max(1, rnd.nextInt(5)); i++)
+                map.put(new DemoComputeJob(), node);
+        }
+
+        return map;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ComputeJobResultPolicy result(ComputeJobResult res, List<ComputeJobResult> rcvd) throws IgniteException {
+        return ComputeJobResultPolicy.REDUCE;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable @Override public Integer reduce(List<ComputeJobResult> results) throws IgniteException {
+        int sum = 0;
+
+        for (ComputeJobResult r: results) {
+            if (!r.isCancelled() && r.getException() == null) {
+                int jobRes = r.getData();
+
+                sum += jobRes;
+            }
+        }
+
+        return sum;
+    }
+
+    /**
+     * Simple compute job.
+     */
+    private static class DemoComputeJob extends ComputeJobAdapter {
+        /** */
+        private static final long serialVersionUID = 0L;
+
+        /** {@inheritDoc} */
+        @Override public Object execute() throws IgniteException {
+            try {
+                Thread.sleep(rnd.nextInt(50));
+
+                return rnd.nextInt(10000);
+            }
+            catch (InterruptedException e) {
+                // Restore interrupt status
+                Thread.currentThread().interrupt();
+            }
+
+            return null;
+        }
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(DemoComputeJob.class, this);
+        }
+    }
+}
diff --git a/modules/web-agent/src/main/resources/log4j.properties b/modules/web-agent/src/main/resources/log4j.properties
new file mode 100644
index 0000000..6e42699
--- /dev/null
+++ b/modules/web-agent/src/main/resources/log4j.properties
@@ -0,0 +1,52 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+log4j.rootLogger=INFO,console_err,file
+
+log4j.logger.org.apache.http=WARN
+log4j.logger.org.apache.ignite.spi.checkpoint.noop.NoopCheckpointSpi=OFF
+log4j.logger.org.apache.ignite.internal.managers.collision.GridCollisionManager=ERROR
+log4j.logger.org.apache.commons.beanutils=WARN
+log4j.logger.sun.net.www.protocol.http=WARN
+
+# Configure console appender.
+log4j.appender.console_err=org.apache.log4j.ConsoleAppender
+log4j.appender.console_err.Threshold=WARN
+log4j.appender.console_err.layout=org.apache.log4j.PatternLayout
+log4j.appender.console_err.layout.ConversionPattern=[%d{ISO8601}][%-5p][%t][%c{1}] %m%n
+
+# Configure console appender.
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+log4j.appender.console.layout.ConversionPattern=[%d{ISO8601}][%-5p][%t][%c{1}] %m%n
+log4j.appender.console.filter.a=org.apache.log4j.varia.LevelMatchFilter
+log4j.appender.console.filter.a.LevelToMatch=INFO
+log4j.appender.console.filter.a.AcceptOnMatch=true
+log4j.appender.console.filter.b=org.apache.log4j.varia.LevelMatchFilter
+log4j.appender.console.filter.b.LevelToMatch=ERROR
+log4j.appender.console.filter.b.AcceptOnMatch=false
+log4j.appender.console.filter.c=org.apache.log4j.varia.LevelMatchFilter
+log4j.appender.console.filter.c.LevelToMatch=WARN
+log4j.appender.console.filter.c.AcceptOnMatch=false
+
+log4j.category.org.apache.ignite.console=INFO,console
+
+# Direct log messages to a log file
+log4j.appender.file=org.apache.log4j.RollingFileAppender
+log4j.appender.file.File=logs/ignite-web-agent.log
+log4j.appender.file.MaxFileSize=10MB
+log4j.appender.file.MaxBackupIndex=10
+log4j.appender.file.layout=org.apache.log4j.PatternLayout
+log4j.appender.file.layout.ConversionPattern=[%d{ISO8601}][%-5p][%t][%c{1}] %m%n
diff --git a/modules/web-agent/src/test/java/org/apache/ignite/console/agent/rest/RestExecutorSelfTest.java b/modules/web-agent/src/test/java/org/apache/ignite/console/agent/rest/RestExecutorSelfTest.java
new file mode 100644
index 0000000..40b5a67
--- /dev/null
+++ b/modules/web-agent/src/test/java/org/apache/ignite/console/agent/rest/RestExecutorSelfTest.java
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.console.agent.rest;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLHandshakeException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.ConnectorConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.processors.rest.protocols.http.jetty.GridJettyObjectMapper;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinder;
+import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+/**
+ * Test for RestExecutor.
+ */
+public class RestExecutorSelfTest {
+    /** Name of the cache created by default in the cluster. */
+    private static final String DEFAULT_CACHE_NAME = "default";
+
+    /** Path to certificates and configs. */
+    private static final String PATH_TO_RESOURCES = "modules/web-agent/src/test/resources/";
+
+    /** JSON object mapper. */
+    private static final ObjectMapper MAPPER = new GridJettyObjectMapper();
+
+    /** */
+    private static final String HTTP_URI = "http://localhost:8080";
+
+    /** */
+    private static final String HTTPS_URI = "https://localhost:8080";
+
+    /** */
+    private static final String JETTY_WITH_SSL = "jetty-with-ssl.xml";
+
+    /** */
+    private static final String JETTY_WITH_CIPHERS_0 = "jetty-with-ciphers-0.xml";
+
+    /** */
+    private static final String JETTY_WITH_CIPHERS_1 = "jetty-with-ciphers-1.xml";
+
+    /** */
+    private static final String JETTY_WITH_CIPHERS_2 = "jetty-with-ciphers-2.xml";
+
+    /** This cipher is disabled by default in JDK 8. */
+    private static final List<String> CIPHER_0 = Collections.singletonList("TLS_DH_anon_WITH_AES_256_GCM_SHA384");
+
+    /** */
+    private static final List<String> CIPHER_1 = Collections.singletonList("TLS_RSA_WITH_NULL_SHA256");
+
+    /** */
+    private static final List<String> CIPHER_2 = Collections.singletonList("TLS_ECDHE_ECDSA_WITH_NULL_SHA");
+
+    /** */
+    private static final List<String> COMMON_CIPHERS = Arrays.asList(
+        "TLS_RSA_WITH_NULL_SHA256",
+        "TLS_ECDHE_ECDSA_WITH_NULL_SHA"
+    );
+
+    /** */
+    @Rule
+    public final ExpectedException ruleForExpectedException = ExpectedException.none();
+
+    /**
+     * @param jettyCfg Optional path to file with Jetty XML config.
+     * @return Prepare configuration for cluster node.
+     */
+    private IgniteConfiguration nodeConfiguration(String jettyCfg) {
+        TcpDiscoveryIpFinder ipFinder = new TcpDiscoveryVmIpFinder();
+
+        ipFinder.registerAddresses(Collections.singletonList(new InetSocketAddress("127.0.0.1", 47500)));
+
+        TcpDiscoverySpi discoverySpi = new TcpDiscoverySpi();
+
+        discoverySpi.setIpFinder(ipFinder);
+
+        IgniteConfiguration cfg = new IgniteConfiguration();
+
+        cfg.setDiscoverySpi(discoverySpi);
+
+        CacheConfiguration<Integer, String> dfltCacheCfg = new CacheConfiguration<>(DEFAULT_CACHE_NAME);
+
+        cfg.setCacheConfiguration(dfltCacheCfg);
+
+        cfg.setIgniteInstanceName(UUID.randomUUID().toString());
+
+        if (!F.isEmpty(jettyCfg)) {
+            ConnectorConfiguration conCfg = new ConnectorConfiguration();
+            conCfg.setJettyPath(resolvePath(jettyCfg));
+
+            cfg.setConnectorConfiguration(conCfg);
+        }
+
+        return cfg;
+    }
+
+    /**
+     * Convert response to JSON.
+     *
+     * @param res REST result.
+     * @return JSON object.
+     * @throws IOException If failed to parse.
+     */
+    private JsonNode toJson(RestResult res) throws IOException {
+        Assert.assertNotNull(res);
+
+        String data = res.getData();
+
+        Assert.assertNotNull(data);
+        Assert.assertFalse(data.isEmpty());
+
+        return MAPPER.readTree(data);
+    }
+
+    /**
+     * @param file File name.
+     * @return Path to file.
+     */
+    private String resolvePath(String file) {
+        return IgniteUtils.resolveIgnitePath(PATH_TO_RESOURCES + file).getAbsolutePath();
+    }
+
+    /**
+     * Try to execute REST command and check response.
+     *
+     * @param nodeCfg Node configuration.
+     * @param uri Node URI.
+     * @param keyStore Key store.
+     * @param keyStorePwd Key store password.
+     * @param trustStore Trust store.
+     * @param trustStorePwd Trust store password.
+     * @param cipherSuites Cipher suites.
+     * @throws Exception If failed.
+     */
+    private void checkRest(
+        IgniteConfiguration nodeCfg,
+        String uri,
+        String keyStore,
+        String keyStorePwd,
+        String trustStore,
+        String trustStorePwd,
+        List<String> cipherSuites
+    ) throws Exception {
+        try (
+            Ignite ignite = Ignition.getOrStart(nodeCfg);
+            RestExecutor exec = new RestExecutor(false, keyStore, keyStorePwd, trustStore, trustStorePwd, cipherSuites)
+        ) {
+            Map<String, Object> params = new HashMap<>();
+            params.put("cmd", "top");
+            params.put("attr", false);
+            params.put("mtr", false);
+            params.put("caches", false);
+
+            RestResult res = exec.sendRequest(Collections.singletonList(uri), params, null);
+
+            JsonNode json = toJson(res);
+
+            Assert.assertTrue(json.isArray());
+
+            for (JsonNode item : json) {
+                Assert.assertTrue(item.get("attributes").isNull());
+                Assert.assertTrue(item.get("metrics").isNull());
+                Assert.assertTrue(item.get("caches").isNull());
+            }
+        }
+    }
+
+    /** */
+    @Test
+    public void nodeNoSslAgentNoSsl() throws Exception {
+        checkRest(
+            nodeConfiguration(""),
+            HTTP_URI,
+            null, null,
+            null, null,
+            null
+        );
+    }
+
+    /** */
+    @Test
+    public void nodeNoSslAgentWithSsl() throws Exception {
+        // Check Web Agent with SSL.
+        ruleForExpectedException.expect(SSLException.class);
+        checkRest(
+            nodeConfiguration(""),
+            HTTPS_URI,
+            resolvePath("client.jks"), "123456",
+            resolvePath("ca.jks"), "123456",
+            null
+        );
+    }
+
+    /** */
+    @Test
+    public void nodeWithSslAgentNoSsl() throws Exception {
+        ruleForExpectedException.expect(IOException.class);
+        checkRest(
+            nodeConfiguration(JETTY_WITH_SSL),
+            HTTP_URI,
+            null, null,
+            null, null,
+            null
+        );
+    }
+
+    /** */
+    @Test
+    public void nodeWithSslAgentWithSsl() throws Exception {
+        checkRest(
+            nodeConfiguration(JETTY_WITH_SSL),
+            HTTPS_URI,
+            resolvePath("client.jks"), "123456",
+            resolvePath("ca.jks"), "123456",
+            null
+        );
+    }
+
+    /** */
+    @Test
+    public void nodeNoCiphersAgentWithCiphers() throws Exception {
+        ruleForExpectedException.expect(SSLHandshakeException.class);
+        checkRest(
+            nodeConfiguration(JETTY_WITH_SSL),
+            HTTPS_URI,
+            resolvePath("client.jks"), "123456",
+            resolvePath("ca.jks"), "123456",
+            CIPHER_0
+        );
+   }
+
+    /** */
+    @Test
+    public void nodeWithCiphersAgentNoCiphers() throws Exception {
+        ruleForExpectedException.expect(SSLHandshakeException.class);
+        checkRest(
+            nodeConfiguration(JETTY_WITH_CIPHERS_0),
+            HTTPS_URI,
+            resolvePath("client.jks"), "123456",
+            resolvePath("ca.jks"), "123456",
+            null
+        );
+   }
+
+    /** */
+    @Test
+    public void nodeWithCiphersAgentWithCiphers() throws Exception {
+        checkRest(
+            nodeConfiguration(JETTY_WITH_CIPHERS_1),
+            HTTPS_URI,
+            resolvePath("client.jks"), "123456",
+            resolvePath("ca.jks"), "123456",
+            CIPHER_1
+        );
+   }
+
+    /** */
+    @Test
+    public void differentCiphers1() throws Exception {
+        ruleForExpectedException.expect(SSLHandshakeException.class);
+        checkRest(
+            nodeConfiguration(JETTY_WITH_CIPHERS_1),
+            HTTPS_URI,
+            resolvePath("client.jks"), "123456",
+            resolvePath("ca.jks"), "123456",
+            CIPHER_2
+        );
+   }
+
+    /** */
+    @Test
+    public void differentCiphers2() throws Exception {
+        ruleForExpectedException.expect(SSLException.class);
+        checkRest(
+            nodeConfiguration(JETTY_WITH_CIPHERS_2),
+            HTTPS_URI,
+            resolvePath("client.jks"), "123456",
+            resolvePath("ca.jks"), "123456",
+            CIPHER_1
+        );
+   }
+
+    /** */
+    @Test
+    public void commonCiphers() throws Exception {
+        checkRest(
+            nodeConfiguration(JETTY_WITH_CIPHERS_1),
+            HTTPS_URI,
+            resolvePath("client.jks"), "123456",
+            resolvePath("ca.jks"), "123456",
+            COMMON_CIPHERS
+        );
+   }
+}
diff --git a/modules/web-agent/src/test/java/org/apache/ignite/testsuites/IgniteWebAgentTestSuite.java b/modules/web-agent/src/test/java/org/apache/ignite/testsuites/IgniteWebAgentTestSuite.java
new file mode 100644
index 0000000..d0bc238
--- /dev/null
+++ b/modules/web-agent/src/test/java/org/apache/ignite/testsuites/IgniteWebAgentTestSuite.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.testsuites;
+
+import org.apache.ignite.console.agent.rest.RestExecutorSelfTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * Web Agent tests.
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    RestExecutorSelfTest.class
+})
+public class IgniteWebAgentTestSuite {
+    // No-op.
+}
diff --git a/modules/web-agent/src/test/resources/ca.jks b/modules/web-agent/src/test/resources/ca.jks
new file mode 100644
index 0000000..9d50bcb
--- /dev/null
+++ b/modules/web-agent/src/test/resources/ca.jks
Binary files differ
diff --git a/modules/web-agent/src/test/resources/client.jks b/modules/web-agent/src/test/resources/client.jks
new file mode 100644
index 0000000..197c75b
--- /dev/null
+++ b/modules/web-agent/src/test/resources/client.jks
Binary files differ
diff --git a/modules/web-agent/src/test/resources/generate.bat b/modules/web-agent/src/test/resources/generate.bat
new file mode 100644
index 0000000..7bc87f1
--- /dev/null
+++ b/modules/web-agent/src/test/resources/generate.bat
@@ -0,0 +1,122 @@
+::
+:: Licensed to the Apache Software Foundation (ASF) under one or more
+:: contributor license agreements.  See the NOTICE file distributed with
+:: this work for additional information regarding copyright ownership.
+:: The ASF licenses this file to You under the Apache License, Version 2.0
+:: (the "License"); you may not use this file except in compliance with
+:: the License.  You may obtain a copy of the License at
+::
+::      http://www.apache.org/licenses/LICENSE-2.0
+::
+:: Unless required by applicable law or agreed to in writing, software
+:: distributed under the License is distributed on an "AS IS" BASIS,
+:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+:: See the License for the specific language governing permissions and
+:: limitations under the License.
+::
+
+::
+:: SSL certificates generation.
+::
+
+::
+:: Preconditions:
+::  1. If needed, download Open SSL for Windows from "https://wiki.openssl.org/index.php/Binaries".
+::   and unpack it to some folder.
+::  2. If needed, install JDK 8 or newer. We need "keytool" from "JDK/bin."
+::  3. Create "openssl.cnf" in some folder.
+::     You may use "https://github.com/openssl/openssl/blob/master/apps/openssl.cnf" as template.
+::  4. If needed, add "opensll" & "keytool" to PATH variable.
+::
+::  NOTE: In case of custom SERVER_DOMAIN_NAME you may need to tweak your "etc/hosts" file.
+::
+
+:: Set Open SSL variables.
+set RANDFILE=_path_where_open_ssl_was_unpacked\.rnd
+set OPENSSL_CONF=_path_where_open_ssl_was_unpacked\openssl.cnf
+
+:: Certificates password.
+set PWD=p123456
+
+:: Server.
+set SERVER_DOMAIN_NAME=localhost
+set SERVER_EMAIL=support@test.com
+
+:: Client.
+set CLIENT_DOMAIN_NAME=localhost
+set CLIENT_EMAIL=client@test.com
+
+:: Cleanup.
+del server.*
+del client.*
+del ca.*
+
+:: Generate server config.
+(
+echo [req]
+echo prompt                 = no
+echo distinguished_name     = dn
+echo req_extensions         = req_ext
+
+echo [ dn ]
+echo countryName            = RU
+echo stateOrProvinceName    = Test
+echo localityName           = Test
+echo organizationName       = Apache
+echo commonName             = %SERVER_DOMAIN_NAME%
+echo organizationalUnitName = IT
+echo emailAddress           = %SERVER_EMAIL%
+
+echo [ req_ext ]
+echo subjectAltName         = @alt_names
+
+echo [ alt_names ]
+echo DNS.1                  = %SERVER_DOMAIN_NAME%
+) > "server.cnf"
+
+:: Generate client config.
+(
+echo [req]
+echo prompt                 = no
+echo distinguished_name     = dn
+echo req_extensions         = req_ext
+
+echo [ dn ]
+echo countryName            = RU
+echo stateOrProvinceName    = Test
+echo localityName           = Test
+echo organizationName       = Apache
+echo commonName             = %CLIENT_DOMAIN_NAME%
+echo organizationalUnitName = IT
+echo emailAddress           = %CLIENT_EMAIL%
+
+echo [ req_ext ]
+echo subjectAltName         = @alt_names
+
+echo [ alt_names ]
+echo DNS.1                  = %CLIENT_DOMAIN_NAME%
+) > "client.cnf"
+
+:: Generate certificates.
+openssl genrsa -des3 -passout pass:%PWD% -out server.key 1024
+openssl req -new -passin pass:%PWD% -key server.key -config server.cnf -out server.csr
+
+openssl req -new -newkey rsa:1024 -nodes -keyout ca.key -x509 -days 365 -config server.cnf -out ca.crt
+
+openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -extensions req_ext -extfile server.cnf -out server.crt
+openssl rsa -passin pass:%PWD% -in server.key -out server.nopass.key
+
+openssl req -new -utf8 -nameopt multiline,utf8 -newkey rsa:1024 -nodes -keyout client.key -config client.cnf -out client.csr
+openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 02 -out client.crt
+
+openssl pkcs12 -export -in server.crt -inkey server.key -certfile server.crt -out server.p12 -passin pass:%PWD% -passout pass:%PWD%
+openssl pkcs12 -export -in client.crt -inkey client.key -certfile ca.crt -out client.p12 -passout pass:%PWD%
+openssl pkcs12 -export -in ca.crt -inkey ca.key -certfile ca.crt -out ca.p12 -passout pass:%PWD%
+
+keytool -importkeystore -srckeystore server.p12 -srcstoretype PKCS12 -destkeystore server.jks -deststoretype JKS -noprompt -srcstorepass %PWD% -deststorepass %PWD%
+keytool -importkeystore -srckeystore client.p12 -srcstoretype PKCS12 -destkeystore client.jks -deststoretype JKS -noprompt -srcstorepass %PWD% -deststorepass %PWD%
+keytool -importkeystore -srckeystore ca.p12 -srcstoretype PKCS12 -destkeystore ca.jks -deststoretype JKS -noprompt -srcstorepass %PWD% -deststorepass %PWD%
+
+openssl x509 -text -noout -in server.crt
+openssl x509 -text -noout -in client.crt
+openssl x509 -text -noout -in ca.crt
diff --git a/modules/web-agent/src/test/resources/generate.sh b/modules/web-agent/src/test/resources/generate.sh
new file mode 100644
index 0000000..95e62c3
--- /dev/null
+++ b/modules/web-agent/src/test/resources/generate.sh
@@ -0,0 +1,111 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# SSL certificates generation.
+#
+
+#
+# Preconditions:
+#  1. If needed, install Open SSL (for example: "sudo apt-get install openssl")
+#  2. If needed, install JDK 8 or newer. We need "keytool" from "JDK/bin".
+#  3. Create "openssl.cnf" in some folder (for example: "/opt/openssl").
+#     You may use "https://github.com/openssl/openssl/blob/master/apps/openssl.cnf" as template.
+#  4. If needed, add "opensll" & "keytool" to PATH variable.
+#
+#  NOTE: In case of custom SERVER_DOMAIN_NAME you may need to tweak your "etc/hosts" file.
+#
+
+set -x
+
+# Set Open SSL variables.
+OPENSSL_CONF=/opt/openssl/openssl.cnf
+
+# Certificates password.
+PWD=p123456
+
+# Server.
+SERVER_DOMAIN_NAME=localhost
+SERVER_EMAIL=support@test.local
+
+# Client.
+CLIENT_DOMAIN_NAME=localhost
+CLIENT_EMAIL=client@test.local
+
+# Cleanup.
+rm -vf server.*
+rm -vf client.*
+rm -vf ca.*
+
+# Generate server config.
+cat << EOF > server.cnf
+[req]
+prompt                 = no
+distinguished_name     = dn
+req_extensions         = req_ext
+[ dn ]
+countryName            = RU
+stateOrProvinceName    = Moscow
+localityName           = Moscow
+organizationName       = test
+commonName             = ${SERVER_DOMAIN_NAME}
+organizationalUnitName = IT
+emailAddress           = ${SERVER_EMAIL}
+[ req_ext ]
+subjectAltName         = @alt_names
+[ alt_names ]
+DNS.1                  = ${SERVER_DOMAIN_NAME}
+EOF
+
+# Generate client config.
+cat << EOF > client.cnf
+[req]
+prompt                 = no
+distinguished_name     = dn
+req_extensions         = req_ext
+[ dn ]
+countryName            = RU
+stateOrProvinceName    = Moscow
+localityName           = Moscow
+organizationName       = test
+commonName             = ${CLIENT_DOMAIN_NAME}
+organizationalUnitName = IT
+emailAddress           = ${CLIENT_EMAIL}
+[ req_ext ]
+subjectAltName         = @alt_names
+[ alt_names ]
+DNS.1                  = ${CLIENT_DOMAIN_NAME}
+EOF
+
+# Generate certificates.
+openssl genrsa -des3 -passout pass:${PWD} -out server.key 1024
+openssl req -new -passin pass:${PWD} -key server.key -config server.cnf -out server.csr
+openssl req -new -newkey rsa:1024 -nodes -keyout ca.key -x509 -days 365 -config server.cnf -out ca.crt
+openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -extensions req_ext -extfile server.cnf -out server.crt
+openssl rsa -passin pass:${PWD} -in server.key -out server.nopass.key
+openssl req -new -utf8 -nameopt multiline,utf8 -newkey rsa:1024 -nodes -keyout client.key -config client.cnf -out client.csr
+openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 02 -out client.crt
+openssl pkcs12 -export -in server.crt -inkey server.key -certfile server.crt -out server.p12 -passin pass:${PWD} -passout pass:${PWD}
+openssl pkcs12 -export -in client.crt -inkey client.key -certfile ca.crt -out client.p12 -passout pass:${PWD}
+openssl pkcs12 -export -in ca.crt -inkey ca.key -certfile ca.crt -out ca.p12 -passout pass:${PWD}
+keytool -importkeystore -srckeystore server.p12 -srcstoretype PKCS12 -destkeystore server.jks -deststoretype JKS -noprompt -srcstorepass ${PWD} -deststorepass ${PWD}
+keytool -importkeystore -srckeystore client.p12 -srcstoretype PKCS12 -destkeystore client.jks -deststoretype JKS -noprompt -srcstorepass ${PWD} -deststorepass ${PWD}
+keytool -importkeystore -srckeystore ca.p12 -srcstoretype PKCS12 -destkeystore ca.jks -deststoretype JKS -noprompt -srcstorepass ${PWD} -deststorepass ${PWD}
+openssl x509 -text -noout -in server.crt
+openssl x509 -text -noout -in client.crt
+openssl x509 -text -noout -in ca.crt
diff --git a/modules/web-agent/src/test/resources/jetty-with-ciphers-0.xml b/modules/web-agent/src/test/resources/jetty-with-ciphers-0.xml
new file mode 100644
index 0000000..4f26a47
--- /dev/null
+++ b/modules/web-agent/src/test/resources/jetty-with-ciphers-0.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0"?>
+
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+    <Arg name="threadPool">
+        <New class="org.eclipse.jetty.util.thread.QueuedThreadPool">
+            <Set name="minThreads">5</Set>
+            <Set name="maxThreads">10</Set>
+        </New>
+    </Arg>
+
+    <New id="httpsCfg" class="org.eclipse.jetty.server.HttpConfiguration">
+        <Set name="secureScheme">https</Set>
+        <Set name="securePort"><SystemProperty name="IGNITE_JETTY_PORT" default="8080"/></Set>
+        <Set name="sendServerVersion">true</Set>
+        <Set name="sendDateHeader">true</Set>
+        <Call name="addCustomizer">
+            <Arg><New class="org.eclipse.jetty.server.SecureRequestCustomizer"/></Arg>
+        </Call>
+    </New>
+
+    <New id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory">
+        <Set name="keyStorePath">modules/web-agent/src/test/resources/server.jks</Set>
+        <Set name="keyStorePassword">123456</Set>
+        <Set name="trustStorePath">modules/web-agent/src/test/resources/ca.jks</Set>
+        <Set name="trustStorePassword">123456</Set>
+        <Set name="needClientAuth">true</Set>
+        <Set name="includeCipherSuites">
+            <Array type="java.lang.String">
+                <Item>TLS_DH_anon_WITH_AES_256_GCM_SHA384</Item>
+            </Array>
+        </Set>
+    </New>
+
+    <Call name="addConnector">
+        <Arg>
+            <New class="org.eclipse.jetty.server.ServerConnector">
+                <Arg name="server">
+                    <Ref refid="Server"/>
+                </Arg>
+                <Arg name="factories">
+                    <Array type="org.eclipse.jetty.server.ConnectionFactory">
+                        <Item>
+                            <New class="org.eclipse.jetty.server.SslConnectionFactory">
+                                <Arg><Ref refid="sslContextFactory"/></Arg>
+                                <Arg>http/1.1</Arg>
+                            </New>
+                        </Item>
+                        <Item>
+                            <New class="org.eclipse.jetty.server.HttpConnectionFactory">
+                                <Arg><Ref refid="httpsCfg"/></Arg>
+                            </New>
+                        </Item>
+                    </Array>
+                </Arg>
+                <Set name="host"><SystemProperty name="IGNITE_JETTY_HOST" default="localhost"/></Set>
+                <Set name="port"><SystemProperty name="IGNITE_JETTY_PORT" default="8080"/></Set>
+                <Set name="idleTimeout">30000</Set>
+                <Set name="reuseAddress">true</Set>
+            </New>
+        </Arg>
+    </Call>
+
+    <Set name="handler">
+        <New id="Handlers" class="org.eclipse.jetty.server.handler.HandlerCollection">
+            <Set name="handlers">
+                <Array type="org.eclipse.jetty.server.Handler">
+                    <Item>
+                        <New id="Contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection"/>
+                    </Item>
+                </Array>
+            </Set>
+        </New>
+    </Set>
+
+    <Set name="stopAtShutdown">false</Set>
+</Configure>
diff --git a/modules/web-agent/src/test/resources/jetty-with-ciphers-1.xml b/modules/web-agent/src/test/resources/jetty-with-ciphers-1.xml
new file mode 100644
index 0000000..19c77ad
--- /dev/null
+++ b/modules/web-agent/src/test/resources/jetty-with-ciphers-1.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0"?>
+
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+    <Arg name="threadPool">
+        <New class="org.eclipse.jetty.util.thread.QueuedThreadPool">
+            <Set name="minThreads">5</Set>
+            <Set name="maxThreads">10</Set>
+        </New>
+    </Arg>
+
+    <New id="httpsCfg" class="org.eclipse.jetty.server.HttpConfiguration">
+        <Set name="secureScheme">https</Set>
+        <Set name="securePort"><SystemProperty name="IGNITE_JETTY_PORT" default="8080"/></Set>
+        <Set name="sendServerVersion">true</Set>
+        <Set name="sendDateHeader">true</Set>
+        <Call name="addCustomizer">
+            <Arg><New class="org.eclipse.jetty.server.SecureRequestCustomizer"/></Arg>
+        </Call>
+    </New>
+
+    <New id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory">
+        <Set name="keyStorePath">modules/web-agent/src/test/resources/server.jks</Set>
+        <Set name="keyStorePassword">123456</Set>
+        <Set name="trustStorePath">modules/web-agent/src/test/resources/ca.jks</Set>
+        <Set name="trustStorePassword">123456</Set>
+        <Set name="needClientAuth">true</Set>
+        <Set name="includeCipherSuites">
+            <Array type="java.lang.String">
+                <Item>TLS_RSA_WITH_NULL_SHA256</Item>
+            </Array>
+        </Set>
+    </New>
+
+    <Call name="addConnector">
+        <Arg>
+            <New class="org.eclipse.jetty.server.ServerConnector">
+                <Arg name="server">
+                    <Ref refid="Server"/>
+                </Arg>
+                <Arg name="factories">
+                    <Array type="org.eclipse.jetty.server.ConnectionFactory">
+                        <Item>
+                            <New class="org.eclipse.jetty.server.SslConnectionFactory">
+                                <Arg><Ref refid="sslContextFactory"/></Arg>
+                                <Arg>http/1.1</Arg>
+                            </New>
+                        </Item>
+                        <Item>
+                            <New class="org.eclipse.jetty.server.HttpConnectionFactory">
+                                <Arg><Ref refid="httpsCfg"/></Arg>
+                            </New>
+                        </Item>
+                    </Array>
+                </Arg>
+                <Set name="host"><SystemProperty name="IGNITE_JETTY_HOST" default="localhost"/></Set>
+                <Set name="port"><SystemProperty name="IGNITE_JETTY_PORT" default="8080"/></Set>
+                <Set name="idleTimeout">30000</Set>
+                <Set name="reuseAddress">true</Set>
+            </New>
+        </Arg>
+    </Call>
+
+    <Set name="handler">
+        <New id="Handlers" class="org.eclipse.jetty.server.handler.HandlerCollection">
+            <Set name="handlers">
+                <Array type="org.eclipse.jetty.server.Handler">
+                    <Item>
+                        <New id="Contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection"/>
+                    </Item>
+                </Array>
+            </Set>
+        </New>
+    </Set>
+
+    <Set name="stopAtShutdown">false</Set>
+</Configure>
diff --git a/modules/web-agent/src/test/resources/jetty-with-ciphers-2.xml b/modules/web-agent/src/test/resources/jetty-with-ciphers-2.xml
new file mode 100644
index 0000000..5e5065f
--- /dev/null
+++ b/modules/web-agent/src/test/resources/jetty-with-ciphers-2.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0"?>
+
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+    <Arg name="threadPool">
+        <New class="org.eclipse.jetty.util.thread.QueuedThreadPool">
+            <Set name="minThreads">5</Set>
+            <Set name="maxThreads">10</Set>
+        </New>
+    </Arg>
+
+    <New id="httpsCfg" class="org.eclipse.jetty.server.HttpConfiguration">
+        <Set name="secureScheme">https</Set>
+        <Set name="securePort"><SystemProperty name="IGNITE_JETTY_PORT" default="8080"/></Set>
+        <Set name="sendServerVersion">true</Set>
+        <Set name="sendDateHeader">true</Set>
+        <Call name="addCustomizer">
+            <Arg><New class="org.eclipse.jetty.server.SecureRequestCustomizer"/></Arg>
+        </Call>
+    </New>
+
+    <New id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory">
+        <Set name="keyStorePath">modules/web-agent/src/test/resources/server.jks</Set>
+        <Set name="keyStorePassword">123456</Set>
+        <Set name="trustStorePath">modules/web-agent/src/test/resources/ca.jks</Set>
+        <Set name="trustStorePassword">123456</Set>
+        <Set name="needClientAuth">true</Set>
+        <Set name="includeCipherSuites">
+            <Array type="java.lang.String">
+                <Item>TLS_ECDHE_ECDSA_WITH_NULL_SHA</Item>
+            </Array>
+        </Set>
+    </New>
+
+    <Call name="addConnector">
+        <Arg>
+            <New class="org.eclipse.jetty.server.ServerConnector">
+                <Arg name="server">
+                    <Ref refid="Server"/>
+                </Arg>
+                <Arg name="factories">
+                    <Array type="org.eclipse.jetty.server.ConnectionFactory">
+                        <Item>
+                            <New class="org.eclipse.jetty.server.SslConnectionFactory">
+                                <Arg><Ref refid="sslContextFactory"/></Arg>
+                                <Arg>http/1.1</Arg>
+                            </New>
+                        </Item>
+                        <Item>
+                            <New class="org.eclipse.jetty.server.HttpConnectionFactory">
+                                <Arg><Ref refid="httpsCfg"/></Arg>
+                            </New>
+                        </Item>
+                    </Array>
+                </Arg>
+                <Set name="host"><SystemProperty name="IGNITE_JETTY_HOST" default="localhost"/></Set>
+                <Set name="port"><SystemProperty name="IGNITE_JETTY_PORT" default="8080"/></Set>
+                <Set name="idleTimeout">30000</Set>
+                <Set name="reuseAddress">true</Set>
+            </New>
+        </Arg>
+    </Call>
+
+    <Set name="handler">
+        <New id="Handlers" class="org.eclipse.jetty.server.handler.HandlerCollection">
+            <Set name="handlers">
+                <Array type="org.eclipse.jetty.server.Handler">
+                    <Item>
+                        <New id="Contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection"/>
+                    </Item>
+                </Array>
+            </Set>
+        </New>
+    </Set>
+
+    <Set name="stopAtShutdown">false</Set>
+</Configure>
diff --git a/modules/web-agent/src/test/resources/jetty-with-ssl.xml b/modules/web-agent/src/test/resources/jetty-with-ssl.xml
new file mode 100644
index 0000000..439f923
--- /dev/null
+++ b/modules/web-agent/src/test/resources/jetty-with-ssl.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0"?>
+
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+    <Arg name="threadPool">
+        <New class="org.eclipse.jetty.util.thread.QueuedThreadPool">
+            <Set name="minThreads">5</Set>
+            <Set name="maxThreads">10</Set>
+        </New>
+    </Arg>
+
+    <New id="httpsCfg" class="org.eclipse.jetty.server.HttpConfiguration">
+        <Set name="secureScheme">https</Set>
+        <Set name="securePort"><SystemProperty name="IGNITE_JETTY_PORT" default="8080"/></Set>
+        <Set name="sendServerVersion">true</Set>
+        <Set name="sendDateHeader">true</Set>
+        <Call name="addCustomizer">
+            <Arg><New class="org.eclipse.jetty.server.SecureRequestCustomizer"/></Arg>
+        </Call>
+    </New>
+
+    <New id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory">
+        <Set name="keyStorePath">modules/web-agent/src/test/resources/server.jks</Set>
+        <Set name="keyStorePassword">123456</Set>
+        <Set name="trustStorePath">modules/web-agent/src/test/resources/ca.jks</Set>
+        <Set name="trustStorePassword">123456</Set>
+        <Set name="needClientAuth">true</Set>
+    </New>
+
+    <Call name="addConnector">
+        <Arg>
+            <New class="org.eclipse.jetty.server.ServerConnector">
+                <Arg name="server">
+                    <Ref refid="Server"/>
+                </Arg>
+                <Arg name="factories">
+                    <Array type="org.eclipse.jetty.server.ConnectionFactory">
+                        <Item>
+                            <New class="org.eclipse.jetty.server.SslConnectionFactory">
+                                <Arg><Ref refid="sslContextFactory"/></Arg>
+                                <Arg>http/1.1</Arg>
+                            </New>
+                        </Item>
+                        <Item>
+                            <New class="org.eclipse.jetty.server.HttpConnectionFactory">
+                                <Arg><Ref refid="httpsCfg"/></Arg>
+                            </New>
+                        </Item>
+                    </Array>
+                </Arg>
+                <Set name="host"><SystemProperty name="IGNITE_JETTY_HOST" default="localhost"/></Set>
+                <Set name="port"><SystemProperty name="IGNITE_JETTY_PORT" default="8080"/></Set>
+                <Set name="idleTimeout">30000</Set>
+                <Set name="reuseAddress">true</Set>
+            </New>
+        </Arg>
+    </Call>
+
+    <Set name="handler">
+        <New id="Handlers" class="org.eclipse.jetty.server.handler.HandlerCollection">
+            <Set name="handlers">
+                <Array type="org.eclipse.jetty.server.Handler">
+                    <Item>
+                        <New id="Contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection"/>
+                    </Item>
+                </Array>
+            </Set>
+        </New>
+    </Set>
+
+    <Set name="stopAtShutdown">false</Set>
+</Configure>
diff --git a/modules/web-agent/src/test/resources/server.jks b/modules/web-agent/src/test/resources/server.jks
new file mode 100644
index 0000000..c673bb0
--- /dev/null
+++ b/modules/web-agent/src/test/resources/server.jks
Binary files differ
diff --git a/parent/pom.xml b/parent/pom.xml
new file mode 100644
index 0000000..0943e72
--- /dev/null
+++ b/parent/pom.xml
@@ -0,0 +1,230 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<!--
+    POM file.
+-->
+<project
+        xmlns="http://maven.apache.org/POM/4.0.0"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache</groupId>
+        <artifactId>apache</artifactId>
+        <version>16</version>
+    </parent>
+
+    <properties>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+
+        <!-- Dependency versions -->
+        <ignite.version>2.8.1</ignite.version>
+        <jackson.version>2.9.10</jackson.version>
+        <slf4j.version>1.7.7</slf4j.version>
+
+        <!-- Maven plugins versions -->
+        <maven.javadoc.plugin.version>3.2.0</maven.javadoc.plugin.version>
+    </properties>
+
+    <groupId>org.apache.ignite</groupId>
+    <artifactId>ignite-parent</artifactId>
+    <version>1</version>
+    <packaging>pom</packaging>
+
+    <url>http://ignite.apache.org</url>
+
+    <description>Web Console for Apache Ignite.</description>
+
+    <licenses>
+        <license>
+            <name>The Apache Software License, Version 2.0</name>
+            <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+        </license>
+    </licenses>
+
+    <mailingLists>
+        <mailingList>
+            <name>Ignite Dev List</name>
+            <subscribe>dev-subscribe@ignite.apache.org</subscribe>
+            <unsubscribe>dev-unsubscribe@ignite.apache.org</unsubscribe>
+            <post>dev@ignite.apache.org</post>
+            <archive>http://mail-archives.apache.org/mod_mbox/ignite-dev</archive>
+        </mailingList>
+    </mailingLists>
+
+    <issueManagement>
+        <system>jira</system>
+        <url>http://issues.apache.org/jira/browse/IGNITE</url>
+    </issueManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.13</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <profiles>
+        <profile>
+            <id>checkstyle</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-checkstyle-plugin</artifactId>
+                        <version>${maven.checkstyle.plugin.version}</version>
+                        <executions>
+                            <execution>
+                                <id>style</id>
+                                <goals>
+                                    <goal>check</goal>
+                                </goals>
+                                <phase>compile</phase>
+                                <configuration>
+                                    <consoleOutput>true</consoleOutput>
+                                    <logViolationsToConsole>true</logViolationsToConsole>
+                                    <failsOnError>true</failsOnError>
+                                    <failOnViolation>true</failOnViolation>
+                                    <outputFile>${project.build.directory}/checkstyle-result.xml</outputFile>
+                                    <configLocation>../checkstyle/checkstyle.xml</configLocation>
+                                    <suppressionsLocation>../checkstyle/checkstyle-suppressions.xml</suppressionsLocation>
+                                    <includeTestSourceDirectory>true</includeTestSourceDirectory>
+                                    <excludes>**/generated/**/*</excludes>
+                                </configuration>
+                            </execution>
+                        </executions>
+                        <dependencies>
+                            <dependency>
+                                <groupId>org.apache.ignite</groupId>
+                                <artifactId>ignite-tools</artifactId>
+                                <version>${project.version}</version>
+                            </dependency>
+                            <dependency>
+                                <groupId>com.puppycrawl.tools</groupId>
+                                <artifactId>checkstyle</artifactId>
+                                <version>${checkstyle.puppycrawl.version}</version>
+                            </dependency>
+                        </dependencies>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+
+        <profile>
+            <id>check-licenses</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.rat</groupId>
+                        <artifactId>apache-rat-plugin</artifactId>
+                        <version>0.12</version>
+                        <configuration>
+                            <addDefaultLicenseMatchers>false</addDefaultLicenseMatchers>
+                            <licenses>
+                                <license implementation="org.apache.rat.analysis.license.FullTextMatchingLicense">
+                                    <licenseFamilyCategory>IAL20</licenseFamilyCategory>
+                                    <licenseFamilyName>Ignite Apache License 2.0</licenseFamilyName>
+                                    <fullText>
+                                        Licensed to the Apache Software Foundation (ASF) under one or more
+                                        contributor license agreements.  See the NOTICE file distributed with
+                                        this work for additional information regarding copyright ownership.
+                                        The ASF licenses this file to You under the Apache License, Version 2.0
+                                        (the "License"); you may not use this file except in compliance with
+                                        the License.  You may obtain a copy of the License at
+
+                                        http://www.apache.org/licenses/LICENSE-2.0
+
+                                        Unless required by applicable law or agreed to in writing, software
+                                        distributed under the License is distributed on an "AS IS" BASIS,
+                                        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+                                        See the License for the specific language governing permissions and
+                                        limitations under the License.
+                                    </fullText>
+                                </license>
+                            </licenses>
+                            <licenseFamilies>
+                                <licenseFamily implementation="org.apache.rat.license.SimpleLicenseFamily">
+                                    <familyName>Ignite Apache License 2.0</familyName>
+                                </licenseFamily>
+                            </licenseFamilies>
+                        </configuration>
+                        <executions>
+                            <execution>
+                                <phase>validate</phase>
+                                <goals>
+                                    <goal>check</goal>
+                                </goals>
+                                <configuration>
+                                    <excludes>
+                                        <exclude>work/**</exclude>
+                                        <exclude>**/target/**</exclude>
+                                        <exclude>**/*.log</exclude>
+                                        <exclude>**/licenses/*.txt</exclude><!--files of licenses-->
+                                        <exclude>**/*readme*.txt</exclude><!--readme files-->
+                                        <exclude>**/*.sql</exclude><!--sql files-->
+                                        <exclude>**/*README*.txt</exclude><!--readme files-->
+                                        <exclude>**/*README*.md</exclude><!--readme files-->
+                                        <exclude>**/*CONTRIBUTING*.md</exclude><!--readme files-->
+                                        <exclude>**/*index*.md</exclude><!--readme files-->
+                                        <exclude>**/*.timestamp</exclude><!--tmp-files-->
+                                        <exclude>**/*.iml</exclude><!--IDEA files-->
+                                        <exclude>**/*.csv</exclude><!--CSV files-->
+                                        <exclude>**/*.jks</exclude><!--bin-files-->
+                                        <exclude>**/pom-installed.xml</exclude><!--tmp-files-->
+                                        <exclude>**/keystore</exclude><!--bin-files-->
+                                        <exclude>**/keystore/*.jks</exclude><!--bin-files-->
+                                        <exclude>**/keystore/*.pem</exclude><!--auto generated files-->
+                                        <exclude>**/keystore/*.pfx</exclude><!--bin-files-->
+                                        <exclude>**/keystore/ca/*.jks</exclude><!--bin-files-->
+                                        <exclude>**/keystore/ca/*.key</exclude><!--bin-files-->
+                                        <exclude>**/keystore/ca/*.txt</exclude><!--auto generated files-->
+                                        <exclude>**/keystore/ca/*.txt.attr</exclude><!--auto generated files-->
+                                        <exclude>**/keystore/ca/*serial</exclude><!--auto generated files-->
+                                        <exclude>**/META-INF/services/**</exclude> <!-- Interface mappings: cannot be changed -->
+
+                                        <!-- Web Console -->
+                                        <exclude>**/web-console/**/.eslintrc</exclude>
+                                        <exclude>**/web-console/**/.babelrc</exclude>
+                                        <exclude>**/web-console/**/*.json</exclude>
+                                        <exclude>**/web-console/**/*.json.sample</exclude>
+                                        <exclude>**/web-console/backend/build/**</exclude>
+                                        <exclude>**/web-console/backend/node_modules/**</exclude>
+                                        <exclude>**/web-console/e2e/testcafe/node_modules/**</exclude>
+                                        <exclude>**/web-console/frontend/build/**</exclude>
+                                        <exclude>**/web-console/frontend/node_modules/**</exclude>
+                                        <exclude>**/web-console/frontend/**/*.png</exclude>
+                                        <exclude>**/web-console/frontend/**/*.svg</exclude>
+
+                                        <!-- Packaging -->
+                                        <exclude>packaging/**</exclude>
+                                    </excludes>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+</project>
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..c18cf81
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,387 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<!--
+    POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.ignite</groupId>
+        <artifactId>ignite-parent</artifactId>
+        <version>1</version>
+        <relativePath>parent</relativePath>
+    </parent>
+
+    <artifactId>ignite-web-console</artifactId>
+    <version>2.10.0-SNAPSHOT</version>
+    <url>http://ignite.apache.org</url>
+    <packaging>pom</packaging>
+
+    <properties>
+        <node.version>v8.11.2</node.version>
+        <docker.registry.host>docker.io</docker.registry.host>
+        <docker.repository>apacheignite</docker.repository>
+        <docker.backend.image>web-console-backend</docker.backend.image>
+        <docker.frontend.image>web-console-frontend</docker.frontend.image>
+        <docker.standalone.image>web-console-standalone</docker.standalone.image>
+    </properties>
+
+    <modules>
+        <module>modules/web-agent</module>
+        <module>modules/compatibility</module>
+    </modules>
+
+    <profiles>
+        <profile>
+            <id>direct-install</id>
+
+            <build>
+                <pluginManagement>
+                    <plugins>
+                        <plugin>
+                            <groupId>com.github.eirslett</groupId>
+                            <artifactId>frontend-maven-plugin</artifactId>
+                            <version>1.6</version>
+                            <configuration>
+                                <nodeVersion>${node.version}</nodeVersion>
+                                <installDirectory>target</installDirectory>
+                            </configuration>
+                        </plugin>
+                    </plugins>
+                </pluginManagement>
+
+                <plugins>
+                    <plugin>
+                        <groupId>com.github.eirslett</groupId>
+                        <artifactId>frontend-maven-plugin</artifactId>
+
+                        <executions>
+                            <execution>
+                                <id>install node and npm for frontend</id>
+                                <goals>
+                                    <goal>install-node-and-npm</goal>
+                                </goals>
+                            </execution>
+
+                            <execution>
+                                <id>download dependencies for frontend</id>
+                                <goals>
+                                    <goal>npm</goal>
+                                </goals>
+
+                                <configuration>
+                                    <workingDirectory>frontend</workingDirectory>
+                                    <arguments>install --no-optional</arguments>
+                                </configuration>
+                            </execution>
+
+                            <execution>
+                                <id>download dependencies for backend</id>
+                                <goals>
+                                    <goal>npm</goal>
+                                </goals>
+
+                                <configuration>
+                                    <workingDirectory>backend</workingDirectory>
+                                    <arguments>install --no-optional --production</arguments>
+                                </configuration>
+                            </execution>
+
+                            <execution>
+                                <id>build frontend</id>
+                                <goals>
+                                    <goal>npm</goal>
+                                </goals>
+
+                                <phase>compile</phase>
+
+                                <configuration>
+                                    <workingDirectory>frontend</workingDirectory>
+                                    <arguments>run build</arguments>
+                                    <environmentVariables>
+                                        <NODE_ENV>production</NODE_ENV>
+                                    </environmentVariables>
+                                </configuration>
+                            </execution>
+
+                            <execution>
+                                <id>build backend</id>
+                                <goals>
+                                    <goal>npm</goal>
+                                </goals>
+
+                                <phase>compile</phase>
+
+                                <configuration>
+                                    <workingDirectory>backend</workingDirectory>
+                                    <arguments>run build</arguments>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-clean-plugin</artifactId>
+                        <version>2.5</version>
+                        <executions>
+                            <execution>
+                                <id>clean-frontend-build</id>
+                                <goals>
+                                    <goal>clean</goal>
+                                </goals>
+                                <phase>process-resources</phase>
+                                <configuration>
+                                    <excludeDefaultDirectories>true</excludeDefaultDirectories>
+                                    <filesets>
+                                        <fileset>
+                                            <directory>${project.basedir}/frontend/build</directory>
+                                        </fileset>
+                                    </filesets>
+                                </configuration>
+                            </execution>
+
+                            <execution>
+                                <id>clean-backend-build</id>
+                                <goals>
+                                    <goal>clean</goal>
+                                </goals>
+                                <phase>process-resources</phase>
+                                <configuration>
+                                    <excludeDefaultDirectories>true</excludeDefaultDirectories>
+                                    <filesets>
+                                        <fileset>
+                                            <directory>${project.basedir}/backend/build</directory>
+                                        </fileset>
+                                    </filesets>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-antrun-plugin</artifactId>
+                        <version>1.7</version>
+
+                        <dependencies>
+                            <dependency>
+                                <groupId>ant-contrib</groupId>
+                                <artifactId>ant-contrib</artifactId>
+                                <version>1.0b3</version>
+                                <exclusions>
+                                    <exclusion>
+                                        <groupId>ant</groupId>
+                                        <artifactId>ant</artifactId>
+                                    </exclusion>
+                                </exclusions>
+                            </dependency>
+                        </dependencies>
+                        <executions>
+                            <execution>
+                                <id>fixed-getos-logic-path</id>
+                                <goals>
+                                    <goal>run</goal>
+                                </goals>
+                                <phase>process-resources</phase>
+                                <configuration>
+
+                                    <tasks>
+                                        <taskdef resource="net/sf/antcontrib/antlib.xml"/>
+                                        <if>
+                                            <available
+                                                file="${project.basedir}/backend/node_modules/mongodb-download/node_modules/getos/index.js"/>
+                                            <then>
+                                                <replace dir="${project.basedir}/backend/node_modules/mongodb-download/node_modules/getos"
+                                                         token='"./logic/"' value='__dirname+"/logic/"'>
+                                                    <include name="index.js"/>
+                                                </replace>
+                                            </then>
+                                        </if>
+                                    </tasks>
+                                </configuration>
+                            </execution>
+
+                            <execution>
+                                <id>fixed-rhel-detection</id>
+                                <goals>
+                                    <goal>run</goal>
+                                </goals>
+                                <phase>process-resources</phase>
+                                <configuration>
+                                    <target>
+                                        <replace dir="${project.basedir}/backend/node_modules/mongodb-download/built"
+                                                 token="/rhel/" value="/Red Hat Linux/i.test(os.dist) || /rhel/">
+                                            <include name="mongodb-download.js"/>
+                                        </replace>
+                                    </target>
+                                </configuration>
+                            </execution>
+
+                            <execution>
+                                <id>fixed-download-url</id>
+                                <goals>
+                                    <goal>run</goal>
+                                </goals>
+                                <phase>process-resources</phase>
+                                <configuration>
+                                    <target>
+                                        <replace dir="${project.basedir}/backend/node_modules/mongodb-download/built"
+                                                 token="https://downloads.mongodb.org" value="https://fastdl.mongodb.org">
+                                            <include name="mongodb-download.js"/>
+                                        </replace>
+                                    </target>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-assembly-plugin</artifactId>
+                        <version>2.4</version>
+                        <inherited>false</inherited>
+
+                        <executions>
+                            <execution>
+                                <id>release-direct-install</id>
+                                <phase>package</phase>
+                                <goals>
+                                    <goal>single</goal>
+                                </goals>
+                                <configuration>
+                                    <descriptors>
+                                        <descriptor>assembly/direct-install.xml</descriptor>
+                                    </descriptors>
+                                    <finalName>ignite-web-console-direct-install-${project.version}</finalName>
+                                    <outputDirectory>target</outputDirectory>
+                                    <appendAssemblyId>false</appendAssemblyId>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+
+        <profile>
+            <id>docker-image</id>
+
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.codehaus.mojo</groupId>
+                        <artifactId>exec-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>docker-build-backend</id>
+                                <phase>package</phase>
+                                <goals>
+                                    <goal>exec</goal>
+                                </goals>
+                                <configuration>
+                                    <executable>docker</executable>
+                                    <arguments>
+                                        <argument>build</argument>
+                                        <argument>-f</argument>
+                                        <argument>docker/compose/backend/Dockerfile</argument>
+                                        <argument>-t</argument>
+                                        <argument>
+                                            ${docker.registry.host}/${docker.repository}/${docker.backend.image}:${project.version}
+                                        </argument>
+                                        <argument>${project.basedir}</argument>
+                                    </arguments>
+                                </configuration>
+                            </execution>
+
+                            <execution>
+                                <id>docker-build-frontend</id>
+                                <phase>package</phase>
+                                <goals>
+                                    <goal>exec</goal>
+                                </goals>
+                                <configuration>
+                                    <executable>docker</executable>
+                                    <arguments>
+                                        <argument>build</argument>
+                                        <argument>-f</argument>
+                                        <argument>docker/compose/frontend/Dockerfile</argument>
+                                        <argument>-t</argument>
+                                        <argument>
+                                            ${docker.registry.host}/${docker.repository}/${docker.frontend.image}:${project.version}
+                                        </argument>
+                                        <argument>${project.basedir}</argument>
+                                    </arguments>
+                                </configuration>
+                            </execution>
+
+                            <execution>
+                                <id>docker-push-backend</id>
+                                <phase>deploy</phase>
+                                <goals>
+                                    <goal>exec</goal>
+                                </goals>
+                                <configuration>
+                                    <executable>docker</executable>
+                                    <arguments>
+                                        <argument>push</argument>
+                                        <argument>
+                                            ${docker.registry.host}/${docker.repository}/${docker.backend.image}:${project.version}
+                                        </argument>
+                                    </arguments>
+                                </configuration>
+                            </execution>
+
+                            <execution>
+                                <id>docker-push-frontend</id>
+                                <phase>deploy</phase>
+                                <goals>
+                                    <goal>exec</goal>
+                                </goals>
+                                <configuration>
+                                    <executable>docker</executable>
+                                    <arguments>
+                                        <argument>push</argument>
+                                        <argument>
+                                            ${docker.registry.host}/${docker.repository}/${docker.frontend.image}:${project.version}
+                                        </argument>
+                                    </arguments>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-deploy-plugin</artifactId>
+                <configuration>
+                    <skip>true</skip>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>