http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/modules/states/configuration/summary/summary.controller.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/modules/states/configuration/summary/summary.controller.js
 
b/modules/web-console/frontend/app/modules/states/configuration/summary/summary.controller.js
deleted file mode 100644
index 8f6a6fa..0000000
--- 
a/modules/web-console/frontend/app/modules/states/configuration/summary/summary.controller.js
+++ /dev/null
@@ -1,350 +0,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.
- */
-
-import _ from 'lodash';
-import saver from 'file-saver';
-
-import summaryProjectStructureTemplateUrl from 
'views/configuration/summary-project-structure.tpl.pug';
-
-const escapeFileName = (name) => name.replace(/[\\\/*\"\[\],\.:;|=<>?]/g, 
'-').replace(/ /g, '_');
-
-export default [
-    '$rootScope', '$scope', '$http', 'IgniteLegacyUtils', 'IgniteMessages', 
'IgniteLoading', '$filter', 'IgniteConfigurationResource', 'JavaTypes', 
'IgniteVersion', 'IgniteConfigurationGenerator', 'SpringTransformer', 
'JavaTransformer', 'IgniteDockerGenerator', 'IgniteMavenGenerator', 
'IgnitePropertiesGenerator', 'IgniteReadmeGenerator', 'IgniteFormUtils', 
'IgniteSummaryZipper', 'IgniteActivitiesData',
-    function($root, $scope, $http, LegacyUtils, Messages, Loading, $filter, 
Resource, JavaTypes, Version, generator, spring, java, docker, pom, 
propsGenerator, readme, FormUtils, SummaryZipper, ActivitiesData) {
-        const ctrl = this;
-
-        // Define template urls.
-        ctrl.summaryProjectStructureTemplateUrl = 
summaryProjectStructureTemplateUrl;
-
-        $scope.ui = {
-            isSafari: !!(/constructor/i.test(window.HTMLElement) || 
window.safari),
-            ready: false
-        };
-
-        Loading.start('summaryPage');
-
-        Resource.read()
-            .then(Resource.populate)
-            .then(({clusters}) => {
-                $scope.clusters = clusters;
-                $scope.clustersMap = {};
-                $scope.clustersView = _.map(clusters, (item) => {
-                    const {_id, name} = item;
-
-                    $scope.clustersMap[_id] = item;
-
-                    return {_id, name};
-                });
-
-                Loading.finish('summaryPage');
-
-                $scope.ui.ready = true;
-
-                if (!_.isEmpty(clusters)) {
-                    const idx = sessionStorage.summarySelectedId || 0;
-
-                    $scope.selectItem(clusters[idx]);
-                }
-            })
-            .catch(Messages.showError);
-
-        $scope.contentVisible = (rows, row) => {
-            return !row || !row._id || _.findIndex(rows, (item) => item._id 
=== row._id) >= 0;
-        };
-
-        $scope.widthIsSufficient = FormUtils.widthIsSufficient;
-        $scope.dialects = {};
-
-        $scope.projectStructureOptions = {
-            nodeChildren: 'children',
-            dirSelectable: false,
-            injectClasses: {
-                iExpanded: 'fa fa-folder-open-o',
-                iCollapsed: 'fa fa-folder-o'
-            },
-            equality: (node1, node2) => {
-                return node1 === node2;
-            }
-        };
-
-        const javaConfigFolder = {
-            type: 'folder',
-            name: 'config',
-            children: [
-                { type: 'file', name: 'ClientConfigurationFactory.java' },
-                { type: 'file', name: 'ServerConfigurationFactory.java' }
-            ]
-        };
-
-        const loadFolder = {
-            type: 'folder',
-            name: 'load',
-            children: [
-                { type: 'file', name: 'LoadCaches.java' }
-            ]
-        };
-
-        const javaStartupFolder = {
-            type: 'folder',
-            name: 'startup',
-            children: [
-                { type: 'file', name: 'ClientNodeCodeStartup.java' },
-                { type: 'file', name: 'ClientNodeSpringStartup.java' },
-                { type: 'file', name: 'ServerNodeCodeStartup.java' },
-                { type: 'file', name: 'ServerNodeSpringStartup.java' }
-            ]
-        };
-
-        const demoFolder = {
-            type: 'folder',
-            name: 'demo',
-            children: [
-                { type: 'file', name: 'DemoStartup.java' }
-            ]
-        };
-
-        const clnCfg = { type: 'file', name: 'client.xml' };
-        const srvCfg = { type: 'file', name: 'server.xml' };
-
-        const resourcesFolder = {
-            type: 'folder',
-            name: 'resources',
-            children: [
-                {
-                    type: 'folder',
-                    name: 'META-INF',
-                    children: [clnCfg, srvCfg]
-                }
-            ]
-        };
-
-        const javaFolder = {
-            type: 'folder',
-            name: 'java',
-            children: [
-                {
-                    type: 'folder',
-                    name: 'config',
-                    children: [
-                        javaConfigFolder,
-                        javaStartupFolder
-                    ]
-                }
-            ]
-        };
-
-        const mainFolder = {
-            type: 'folder',
-            name: 'main',
-            children: [javaFolder]
-        };
-
-        const projectStructureRoot = {
-            type: 'folder',
-            name: 'project.zip',
-            children: [
-                {
-                    type: 'folder',
-                    name: 'jdbc-drivers',
-                    children: [
-                        { type: 'file', name: 'README.txt' }
-                    ]
-                },
-                {
-                    type: 'folder',
-                    name: 'src',
-                    children: [mainFolder]
-                },
-                { type: 'file', name: '.dockerignore' },
-                { type: 'file', name: 'Dockerfile' },
-                { type: 'file', name: 'pom.xml' },
-                { type: 'file', name: 'README.txt' }
-            ]
-        };
-
-        $scope.projectStructure = [projectStructureRoot];
-
-        $scope.projectStructureExpanded = [projectStructureRoot];
-
-        $scope.tabsServer = { activeTab: 0 };
-        $scope.tabsClient = { activeTab: 0 };
-
-        /**
-         *
-         * @param {Object} node - Tree node.
-         * @param {string[]} path - Path to find.
-         * @returns {Object} Tree node.
-         */
-        function getOrCreateFolder(node, path) {
-            if (_.isEmpty(path))
-                return node;
-
-            const leaf = path.shift();
-
-            let children = null;
-
-            if (!_.isEmpty(node.children)) {
-                children = _.find(node.children, {type: 'folder', name: leaf});
-
-                if (children)
-                    return getOrCreateFolder(children, path);
-            }
-
-            children = {type: 'folder', name: leaf, children: []};
-
-            node.children.push(children);
-
-            node.children = _.orderBy(node.children, ['type', 'name'], 
['desc', 'asc']);
-
-            return getOrCreateFolder(children, path);
-        }
-
-        function addClass(fullClsName) {
-            const path = fullClsName.split('.');
-            const leaf = {type: 'file', name: path.pop() + '.java'};
-            const folder = getOrCreateFolder(javaFolder, path);
-
-            if (!_.find(folder.children, leaf))
-                folder.children.push(leaf);
-        }
-
-        function cacheHasDatasource(cache) {
-            if (cache.cacheStoreFactory && cache.cacheStoreFactory.kind) {
-                const storeFactory = 
cache.cacheStoreFactory[cache.cacheStoreFactory.kind];
-
-                return !!(storeFactory && (storeFactory.connectVia ? 
(storeFactory.connectVia === 'DataSource' ? storeFactory.dialect : false) : 
storeFactory.dialect)); // eslint-disable-line no-nested-ternary
-            }
-
-            return false;
-        }
-
-        $scope.selectItem = (cluster) => {
-            delete ctrl.cluster;
-
-            if (!cluster)
-                return;
-
-            cluster = $scope.clustersMap[cluster._id];
-
-            ctrl.cluster = cluster;
-
-            $scope.cluster = cluster;
-            $scope.selectedItem = cluster;
-            $scope.dialects = {};
-
-            sessionStorage.summarySelectedId = 
$scope.clusters.indexOf(cluster);
-
-            mainFolder.children = [javaFolder, resourcesFolder];
-
-            if (_.find(cluster.caches, (cache) => 
!_.isNil(cache.cacheStoreFactory)))
-                javaFolder.children = [javaConfigFolder, loadFolder, 
javaStartupFolder];
-            else
-                javaFolder.children = [javaConfigFolder, javaStartupFolder];
-
-            if (_.nonNil(_.find(cluster.caches, cacheHasDatasource)) || 
cluster.sslEnabled)
-                resourcesFolder.children.push({ type: 'file', name: 
'secret.properties' });
-
-            if (java.isDemoConfigured(cluster, $root.IgniteDemoMode))
-                javaFolder.children.push(demoFolder);
-
-            if (cluster.discovery.kind === 'Jdbc' && 
cluster.discovery.Jdbc.dialect)
-                $scope.dialects[cluster.discovery.Jdbc.dialect] = true;
-
-            if (cluster.discovery.kind === 'Kubernetes')
-                resourcesFolder.children.push({ type: 'file', name: 
'ignite-service.yaml' });
-
-            _.forEach(cluster.caches, (cache) => {
-                if (cache.cacheStoreFactory) {
-                    const store = 
cache.cacheStoreFactory[cache.cacheStoreFactory.kind];
-
-                    if (store && store.dialect)
-                        $scope.dialects[store.dialect] = true;
-                }
-
-                _.forEach(cache.domains, (domain) => {
-                    if (domain.generatePojo && _.nonEmpty(domain.keyFields)) {
-                        if (JavaTypes.nonBuiltInClass(domain.keyType))
-                            addClass(domain.keyType);
-
-                        addClass(domain.valueType);
-                    }
-                });
-            });
-
-            projectStructureRoot.name = cluster.name + '-project.zip';
-            clnCfg.name = cluster.name + '-client.xml';
-            srvCfg.name = cluster.name + '-server.xml';
-        };
-
-        $scope.$watch('cluster', (cluster) => {
-            if (!cluster)
-                return;
-
-            if (!$filter('hasPojo')(cluster) && $scope.tabsClient.activeTab 
=== 3)
-                $scope.tabsClient.activeTab = 0;
-        });
-
-        $scope.$watch('cluster._id', () => {
-            $scope.tabsClient.init = [];
-            $scope.tabsServer.init = [];
-        });
-
-        // TODO IGNITE-2114: implemented as independent logic for download.
-        $scope.downloadConfiguration = function() {
-            if ($scope.isPrepareDownloading)
-                return;
-
-            const cluster = $scope.cluster;
-
-            $scope.isPrepareDownloading = true;
-
-            ActivitiesData.post({ action: '/configuration/download' });
-
-            return new SummaryZipper({ cluster, data: ctrl.data || {}, demo: 
$root.IgniteDemoMode, targetVer: Version.currentSbj.getValue() })
-                .then((data) => {
-                    saver.saveAs(data, escapeFileName(cluster.name) + 
'-project.zip');
-                })
-                .catch((err) => Messages.showError('Failed to generate project 
files. ' + err.message))
-                .then(() => $scope.isPrepareDownloading = false);
-        };
-
-        /**
-         * @returns {boolean} 'true' if at least one proprietary JDBC driver 
is configured for cache store.
-         */
-        $scope.downloadJdbcDriversVisible = function() {
-            const dialects = $scope.dialects;
-
-            return !!(dialects.Oracle || dialects.DB2 || dialects.SQLServer);
-        };
-
-        /**
-         * Open download proprietary JDBC driver pages.
-         */
-        $scope.downloadJdbcDrivers = function() {
-            const dialects = $scope.dialects;
-
-            if (dialects.Oracle)
-                
window.open('http://www.oracle.com/technetwork/database/features/jdbc/default-2280470.html');
-
-            if (dialects.DB2)
-                
window.open('http://www-01.ibm.com/support/docview.wss?uid=swg21363866');
-
-            if (dialects.SQLServer)
-                
window.open('https://www.microsoft.com/en-us/download/details.aspx?id=11774');
-        };
-    }
-];

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/modules/states/configuration/summary/summary.worker.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/modules/states/configuration/summary/summary.worker.js
 
b/modules/web-console/frontend/app/modules/states/configuration/summary/summary.worker.js
index 1939906..c80d698 100644
--- 
a/modules/web-console/frontend/app/modules/states/configuration/summary/summary.worker.js
+++ 
b/modules/web-console/frontend/app/modules/states/configuration/summary/summary.worker.js
@@ -26,6 +26,11 @@ import IgniteConfigurationGenerator from 
'app/modules/configuration/generator/Co
 import IgniteJavaTransformer from 
'app/modules/configuration/generator/JavaTransformer.service';
 import IgniteSpringTransformer from 
'app/modules/configuration/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();
@@ -71,8 +76,8 @@ onmessage = function(e) {
 
     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 clientNearCaches = filter(cluster.caches, (cache) =>
+        cache.cacheMode === 'PARTITIONED' && get(cache, 
'clientNearConfiguration.enabled'));
 
     const secProps = properties.generate(cfg);
 
@@ -104,9 +109,9 @@ onmessage = function(e) {
     }
 
     // Generate loader for caches with configured store.
-    const cachesToLoad = _.filter(cluster.caches, (cache) => 
_.nonNil(cache.cacheStoreFactory));
+    const cachesToLoad = filter(cluster.caches, (cache) => 
nonNil(cache.cacheStoreFactory));
 
-    if (_.nonEmpty(cachesToLoad))
+    if (nonEmpty(cachesToLoad))
         zip.file(`${srcPath}/load/LoadCaches.java`, 
java.loadCaches(cachesToLoad, 'load', 'LoadCaches', `"${clientXml}"`));
 
     const startupPath = `${srcPath}/startup`;
@@ -124,8 +129,8 @@ onmessage = function(e) {
     zip.file('README.txt', readme.generate());
     zip.file('jdbc-drivers/README.txt', readme.generateJDBC());
 
-    if (_.isEmpty(data.pojos))
-        data.pojos = java.pojos(cluster.caches);
+    if (isEmpty(data.pojos))
+        data.pojos = java.pojos(cluster.caches, true);
 
     for (const pojo of data.pojos) {
         if (pojo.keyClass)

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/primitives/btn/index.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/btn/index.scss 
b/modules/web-console/frontend/app/primitives/btn/index.scss
index 162fde4..061d411 100644
--- a/modules/web-console/frontend/app/primitives/btn/index.scss
+++ b/modules/web-console/frontend/app/primitives/btn/index.scss
@@ -250,6 +250,14 @@ $btn-content-padding-with-border: 9px 11px;
     @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);
@@ -302,6 +310,10 @@ $btn-content-padding-with-border: 9px 11px;
         $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%);
+    }
 }
 
 @mixin ignite-link($color, $color-hover) {
@@ -336,3 +348,12 @@ $btn-content-padding-with-border: 9px 11px;
         $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%)
+    );
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/primitives/checkbox/index.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/checkbox/index.scss 
b/modules/web-console/frontend/app/primitives/checkbox/index.scss
new file mode 100644
index 0000000..d1e1e83
--- /dev/null
+++ b/modules/web-console/frontend/app/primitives/checkbox/index.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.
+ */
+
+input[type='checkbox'] {
+    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;
+    }
+}
+
+.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;
+        }
+    }    
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/primitives/datepicker/index.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/datepicker/index.pug 
b/modules/web-console/frontend/app/primitives/datepicker/index.pug
index e789a1f..7120111 100644
--- a/modules/web-console/frontend/app/primitives/datepicker/index.pug
+++ b/modules/web-console/frontend/app/primitives/datepicker/index.pug
@@ -22,10 +22,10 @@ mixin ignite-form-field-datepicker(label, model, name, 
mindate, maxdate, minview
 
             placeholder=placeholder
             
-            data-ng-model=model
+            ng-model=model
 
-            data-ng-required=required && `${required}`
-            data-ng-disabled=disabled && `${disabled}`
+            ng-required=required && `${required}`
+            ng-disabled=disabled && `${disabled}`
 
             bs-datepicker
 
@@ -42,8 +42,6 @@ mixin ignite-form-field-datepicker(label, model, name, 
mindate, maxdate, minview
             tabindex='0'
 
             onkeydown='return false'
-
-            data-ignite-form-panel-field=''
         )&attributes(attributes.attributes)
 
     .datepicker--ignite.ignite-form-field

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/primitives/dropdown/index.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/dropdown/index.pug 
b/modules/web-console/frontend/app/primitives/dropdown/index.pug
index c145244..0099457 100644
--- a/modules/web-console/frontend/app/primitives/dropdown/index.pug
+++ b/modules/web-console/frontend/app/primitives/dropdown/index.pug
@@ -17,10 +17,10 @@
 mixin ignite-form-field-bsdropdown({label, model, name, disabled, required, 
options, tip})
     .dropdown--ignite.ignite-form-field
         .btn-ignite.btn-ignite--primary-outline(
-            data-ng-model=model
+            ng-model=model
 
-            data-ng-required=required && `${required}`
-            data-ng-disabled=disabled && `${disabled}` || `!${options}.length`
+            ng-required=required && `${required}`
+            ng-disabled=disabled && `${disabled}` || `!${options}.length`
 
             bs-dropdown=''
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/primitives/file/index.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/file/index.pug 
b/modules/web-console/frontend/app/primitives/file/index.pug
index 4ce9ef4..7bdd3cc 100644
--- a/modules/web-console/frontend/app/primitives/file/index.pug
+++ b/modules/web-console/frontend/app/primitives/file/index.pug
@@ -32,6 +32,6 @@ mixin ignite-form-field-file(label, model, name, disabled, 
required, options, ti
                 input(
                     id=`{{ ${name} }}Input`
                     type='file'
-                    data-ng-model=model
+                    ng-model=model
                 )&attributes(attributes)
                 | {{ `${model}` }}

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/primitives/form-field/index.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/form-field/index.scss 
b/modules/web-console/frontend/app/primitives/form-field/index.scss
index e6c0a58..7d9ea1f 100644
--- a/modules/web-console/frontend/app/primitives/form-field/index.scss
+++ b/modules/web-console/frontend/app/primitives/form-field/index.scss
@@ -18,21 +18,35 @@
 @import '../../../public/stylesheets/variables';
 
 .theme--ignite {
+    [ignite-icon='info'], .tipLabel {
+        color: $ignite-brand-success;
+    }
+
     .ignite-form-field {
         width: 100%;
 
         &.radio--ignite {
             width: auto;
+
         }
 
-        &__label {
-            float: left;
-            width: 100%;
-            margin: 0 10px 4px;
+        &.ignite-form-field-dropdown {
+            .ignite-form-field__control button {
+                display: inline-block;
+                overflow: hidden !important;
+                text-overflow: ellipsis;
+            }
+        }
 
-            color: $gray-light;
-            font-size: 12px;
-            line-height: 12px;
+        [ignite-icon='info'], .tipLabel {
+            margin-left: 4px;
+            flex: 0 0 auto;
+        }
+
+
+        label.required {
+            content: '*';
+            margin-left: 0.25em;
         }
 
         .ignite-form-field__control {
@@ -75,10 +89,20 @@
                     &: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: right;
+                    text-align: left;
                 }
             }
 
@@ -87,6 +111,62 @@
             }
        }
     }
+    .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 0px;
+        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 {

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/primitives/index.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/index.js 
b/modules/web-console/frontend/app/primitives/index.js
index 5a2f45c..f9d8591 100644
--- a/modules/web-console/frontend/app/primitives/index.js
+++ b/modules/web-console/frontend/app/primitives/index.js
@@ -33,4 +33,5 @@ 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';

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/primitives/modal/index.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/modal/index.scss 
b/modules/web-console/frontend/app/primitives/modal/index.scss
index fcf9885..802a241 100644
--- a/modules/web-console/frontend/app/primitives/modal/index.scss
+++ b/modules/web-console/frontend/app/primitives/modal/index.scss
@@ -63,6 +63,7 @@
 
     .modal-body {
         max-height: calc(100vh - 150px);
+        overflow: auto;
     }
 }
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/primitives/radio/index.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/radio/index.pug 
b/modules/web-console/frontend/app/primitives/radio/index.pug
index 2b2223a..f47fd17 100644
--- a/modules/web-console/frontend/app/primitives/radio/index.pug
+++ b/modules/web-console/frontend/app/primitives/radio/index.pug
@@ -26,14 +26,10 @@ mixin form-field-radio(label, model, name, value, disabled, 
required, tip)
                         name=`{{ ${name} }}`
                         type='radio'
 
-                        data-ng-model=model
-                        data-ng-value=value
-                        data-ng-required=required && `${required}`
-                        data-ng-disabled=disabled && `${disabled}`
-
-                        data-ng-focus='tableReset()'
-
-                        data-ignite-form-panel-field=''
+                        ng-model=model
+                        ng-value=value
+                        ng-required=required && `${required}`
+                        ng-disabled=disabled && `${disabled}`
                     )
                     div
             span #{label}

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/primitives/tabs/index.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/tabs/index.scss 
b/modules/web-console/frontend/app/primitives/tabs/index.scss
index 022d66b..811d847 100644
--- a/modules/web-console/frontend/app/primitives/tabs/index.scss
+++ b/modules/web-console/frontend/app/primitives/tabs/index.scss
@@ -23,6 +23,7 @@ ul.tabs {
     $offset-vertical: 11px;
     $offset-horizontal: 25px;
     $font-size: 14px;
+    $border-width: 5px;
 
     list-style: none;
 
@@ -34,10 +35,13 @@ ul.tabs {
     li {
         position: relative;
         top: 1px;
+        height: $height + $border-width;
 
         display: inline-block;
 
-        border-bottom: 5px solid transparent;
+        border-bottom: 0px solid transparent;
+        transition-property: border-bottom;
+        transition-duration: 0.2s;
 
         a {
             display: inline-block;
@@ -61,6 +65,10 @@ ul.tabs {
             }
         }
 
+        &.active, &:hover {
+            border-bottom-width: $border-width;
+        }
+
         &.active {
             border-color: $brand-primary;
         }

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/primitives/timepicker/index.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/timepicker/index.pug 
b/modules/web-console/frontend/app/primitives/timepicker/index.pug
index 9f1f6ec..54ce8c1 100644
--- a/modules/web-console/frontend/app/primitives/timepicker/index.pug
+++ b/modules/web-console/frontend/app/primitives/timepicker/index.pug
@@ -23,10 +23,10 @@ mixin ignite-form-field-timepicker(label, model, name, 
mindate, maxdate, disable
 
             placeholder=placeholder
             
-            data-ng-model=model
+            ng-model=model
 
-            data-ng-required=required && `${required}`
-            data-ng-disabled=disabled && `${disabled}`
+            ng-required=required && `${required}`
+            ng-disabled=disabled && `${disabled}`
 
             bs-timepicker
             data-time-format='HH:mm'
@@ -40,8 +40,6 @@ mixin ignite-form-field-timepicker(label, model, name, 
mindate, maxdate, disable
             tabindex='0'
 
             onkeydown="return false"
-
-            data-ignite-form-panel-field=''
         )&attributes(attributes.attributes)
 
     .timepicker--ignite.ignite-form-field

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/primitives/tooltip/index.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/tooltip/index.pug 
b/modules/web-console/frontend/app/primitives/tooltip/index.pug
index 632fc61..ea6a344 100644
--- a/modules/web-console/frontend/app/primitives/tooltip/index.pug
+++ b/modules/web-console/frontend/app/primitives/tooltip/index.pug
@@ -16,7 +16,8 @@
 
 mixin tooltip(title, options, tipClass = 'tipField')
     if title
-        i.icon-help(
+        svg(
+            ignite-icon='info'
             bs-tooltip=''
 
             data-title=title

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/primitives/ui-grid/index.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/ui-grid/index.scss 
b/modules/web-console/frontend/app/primitives/ui-grid/index.scss
index 25ba390..2ffffff 100644
--- a/modules/web-console/frontend/app/primitives/ui-grid/index.scss
+++ b/modules/web-console/frontend/app/primitives/ui-grid/index.scss
@@ -40,7 +40,7 @@
     }
 
     .ui-grid-cell {
-        height: $height;
+        height: $height - 1px;
 
         border-color: transparent;
     }
@@ -78,6 +78,10 @@
         }
     }
 
+    .ui-grid-row:last-child .ui-grid-cell {
+        border-bottom-width: 0;
+    }
+
     .ui-grid-header-viewport {
         .ui-grid-header-canvas {
             .ui-grid-header-cell {
@@ -239,6 +243,7 @@
 
     .ui-grid-row {
         height: $height;
+        border-bottom: 1px solid $table-border-color;
 
         &:nth-child(odd) {
             .ui-grid-cell {
@@ -252,10 +257,6 @@
             }
         }
 
-        &:not(:first-child) {
-            border-top: 1px solid $table-border-color;
-        }
-
         &.ui-grid-row-selected > [ui-grid-row] > .ui-grid-cell {
             background-color: #e5f2f9;
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/services/Caches.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/Caches.js 
b/modules/web-console/frontend/app/services/Caches.js
index 938094e..add63f8 100644
--- a/modules/web-console/frontend/app/services/Caches.js
+++ b/modules/web-console/frontend/app/services/Caches.js
@@ -15,14 +15,218 @@
  * limitations under the License.
  */
 
+import ObjectID from 'bson-objectid';
+import omit from 'lodash/fp/omit';
+
 export default class Caches {
     static $inject = ['$http'];
 
+    /** @type {ig.menu<ig.config.cache.CacheModes>} */
+    cacheModes = [
+        {value: 'LOCAL', label: 'LOCAL'},
+        {value: 'REPLICATED', label: 'REPLICATED'},
+        {value: 'PARTITIONED', label: 'PARTITIONED'}
+    ];
+
+    /** @type {ig.menu<ig.config.cache.AtomicityModes>} */
+    atomicityModes = [
+        {value: 'ATOMIC', label: 'ATOMIC'},
+        {value: 'TRANSACTIONAL', label: 'TRANSACTIONAL'}
+    ];
+
+    /**
+     * @param {ng.IHttpService} $http
+     */
     constructor($http) {
-        Object.assign(this, {$http});
+        this.$http = $http;
     }
 
     saveCache(cache) {
         return this.$http.post('/api/v1/configuration/caches/save', cache);
     }
+
+    /**
+     * @param {string} cacheID
+     */
+    getCache(cacheID) {
+        return this.$http.get(`/api/v1/configuration/caches/${cacheID}`);
+    }
+
+    /**
+     * @param {string} cacheID
+     */
+    removeCache(cacheID) {
+        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: []
+        };
+    }
+
+    /**
+     * @param {object} cache
+     * @returns {ig.config.cache.ShortCache}
+     */
+    toShortCache(cache) {
+        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'}
+    ];
+
+    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;
+        }
+    };
+
+    /**
+     * @param {ig.config.cache.ShortCache} cache
+     */
+    getCacheBackupsCount(cache) {
+        return this.shouldShowCacheBackupsCount(cache)
+            ? (cache.backups || 0)
+            : void 0;
+    }
+
+    /**
+     * @param {ig.config.cache.ShortCache} cache
+     */
+    shouldShowCacheBackupsCount(cache) {
+        return cache && cache.cacheMode === 'PARTITIONED';
+    }
 }

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/services/Clusters.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/Clusters.js 
b/modules/web-console/frontend/app/services/Clusters.js
index dd2f598..4e057fc 100644
--- a/modules/web-console/frontend/app/services/Clusters.js
+++ b/modules/web-console/frontend/app/services/Clusters.js
@@ -15,9 +15,21 @@
  * limitations under the License.
  */
 
+import get from 'lodash/get';
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/observable/fromPromise';
+import ObjectID from 'bson-objectid/objectid';
+import {uniqueName} from 'app/utils/uniqueName';
+import omit from 'lodash/fp/omit';
+
+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'];
 
+    /** @type {ig.menu<ig.config.cluster.DiscoveryKinds>}>} */
     discoveries = [
         {value: 'Vm', label: 'Static IPs'},
         {value: 'Multicast', label: 'Multicast'},
@@ -30,19 +42,109 @@ export default class Clusters {
         {value: 'Kubernetes', label: 'Kubernetes'}
     ];
 
-    // In bytes
-    minMemoryPolicySize = 10485760;
+    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
+     * @param {ng.IHttpService} $http
+     */
     constructor($http) {
-        Object.assign(this, {$http});
+        this.$http = $http;
+    }
+
+    getConfiguration(clusterID) {
+        return this.$http.get(`/api/v1/configuration/${clusterID}`);
+    }
+
+    getAllConfigurations() {
+        return this.$http.get('/api/v1/configuration/list');
+    }
+
+    getCluster(clusterID) {
+        return this.$http.get(`/api/v1/configuration/clusters/${clusterID}`);
+    }
+
+    getClusterCaches(clusterID) {
+        return 
this.$http.get(`/api/v1/configuration/clusters/${clusterID}/caches`);
+    }
+
+    /**
+     * @param {string} clusterID
+     * @returns {ng.IPromise<ng.IHttpResponse<{data: 
Array<ig.config.model.ShortDomainModel>}>>}
+     */
+    getClusterModels(clusterID) {
+        return 
this.$http.get(`/api/v1/configuration/clusters/${clusterID}/models`);
+    }
+
+    getClusterIGFSs(clusterID) {
+        return 
this.$http.get(`/api/v1/configuration/clusters/${clusterID}/igfss`);
+    }
+
+    /**
+     * @returns {ng.IPromise<ng.IHttpResponse<{data: 
Array<ig.config.cluster.ShortCluster>}>>}
+     */
+    getClustersOverview() {
+        return this.$http.get('/api/v1/configuration/clusters/');
+    }
+
+    getClustersOverview$() {
+        return Observable.fromPromise(this.getClustersOverview());
     }
 
     saveCluster(cluster) {
         return this.$http.post('/api/v1/configuration/clusters/save', cluster);
     }
 
+    saveCluster$(cluster) {
+        return Observable.fromPromise(this.saveCluster(cluster));
+    }
+
+    removeCluster(cluster) {
+        return this.$http.post('/api/v1/configuration/clusters/remove', {_id: 
cluster});
+    }
+
+    removeCluster$(cluster) {
+        return Observable.fromPromise(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: {},
@@ -61,12 +163,15 @@ export default class Clusters {
             swapSpaceSpi: {},
             transactionConfiguration: {},
             dataStorageConfiguration: {
+                pageSize: null,
+                concurrencyLevel: null,
                 defaultDataRegionConfiguration: {
                     name: 'default'
                 },
                 dataRegionConfigurations: []
             },
             memoryConfiguration: {
+                pageSize: null,
                 memoryPolicies: [{
                     name: 'default',
                     maxSize: null
@@ -80,6 +185,9 @@ export default class Clusters {
             sqlConnectorConfiguration: {
                 tcpNoDelay: true
             },
+            clientConnectorConfiguration: {
+                tcpNoDelay: true
+            },
             space: void 0,
             discovery: {
                 kind: 'Multicast',
@@ -95,7 +203,374 @@ export default class Clusters {
             failoverSpi: [],
             logger: {Log4j: { mode: 'Default'}},
             caches: [],
-            igfss: []
+            igfss: [],
+            models: [],
+            checkpointSpi: [],
+            loadBalancingSpi: []
         };
     }
+
+    /** @type {ig.menu<ig.config.cluster.FailoverSPIs>} */
+    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
+        };
+    }
+
+    requiresProprietaryDrivers(cluster) {
+        return get(cluster, 'discovery.kind') === 'Jdbc' && ['Oracle', 'DB2', 
'SQLServer'].includes(get(cluster, 'discovery.Jdbc.dialect'));
+    }
+
+    JDBCDriverURL(cluster) {
+        return ({
+            Oracle: 
'http://www.oracle.com/technetwork/database/features/jdbc/default-2280470.html',
+            DB2: 'http://www-01.ibm.com/support/docview.wss?uid=swg21363866',
+            SQLServer: 
'https://www.microsoft.com/en-us/download/details.aspx?id=11774'
+        })[get(cluster, 'discovery.Jdbc.dialect')];
+    }
+
+    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.05,
+            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;
+            }
+        },
+        subIntervals: {
+            default: 5,
+            min: 1,
+            step: 1
+        },
+        rateTimeInterval: {
+            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());
+    }
+
+    /** @type {ig.menu<ig.config.cluster.LoadBalancingKinds>} */
+    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;
+            }
+        }
+    };
+
+    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;
+    }
 }

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/services/Confirm.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/Confirm.service.js 
b/modules/web-console/frontend/app/services/Confirm.service.js
index 6fe7ab8..c2eaf35 100644
--- a/modules/web-console/frontend/app/services/Confirm.service.js
+++ b/modules/web-console/frontend/app/services/Confirm.service.js
@@ -18,6 +18,44 @@
 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', ($scope) => {
+                    $scope.yesNo = yesNo;
+                    $scope.content = content;
+                    $scope.confirmCancel = $scope.confirmNo = () => {
+                        reject(new CancellationError());
+                        $scope.$hide();
+                    };
+                    $scope.confirmYes = () => {
+                        resolve();
+                        $scope.$hide();
+                    };
+                }]
+            });
+        });
+    }
+}
+
 // Confirm popup service.
 export default ['IgniteConfirm', ['$rootScope', '$q', '$modal', '$animate', 
($root, $q, $modal, $animate) => {
     const scope = $root.$new();

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/services/ConfirmBatch.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/ConfirmBatch.service.js 
b/modules/web-console/frontend/app/services/ConfirmBatch.service.js
index 2739f29..5c6961c 100644
--- a/modules/web-console/frontend/app/services/ConfirmBatch.service.js
+++ b/modules/web-console/frontend/app/services/ConfirmBatch.service.js
@@ -19,65 +19,72 @@ import templateUrl from 
'views/templates/batch-confirm.tpl.pug';
 import {CancellationError} from 'app/errors/CancellationError';
 
 // Service for confirm or skip several steps.
-export default ['IgniteConfirmBatch', ['$rootScope', '$q', '$modal', ($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);
-    };
-
-    return {
+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.
-         *
-         * @param confirmMessageFn Function to generate a confirm message.
-         * @param itemsToConfirm Array of element to process by confirm.
+         * @template T
+         * @param {(T) => string} confirmMessageFn Function to generate a 
confirm message.
+         * @param {Array<T>} [itemsToConfirm] Array of element to process by 
confirm.
          */
-        confirm(confirmMessageFn, itemsToConfirm) {
+        this.confirm = function confirm(confirmMessageFn, itemsToConfirm) {
             scope.deferred = $q.defer();
 
             scope.contentGenerator = confirmMessageFn;
@@ -89,6 +96,6 @@ export default ['IgniteConfirmBatch', ['$rootScope', '$q', 
'$modal', ($root, $q,
             scope.confirmModal.$promise.then(scope.confirmModal.show);
 
             return scope.deferred.promise;
-        }
-    };
-}]];
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/services/ErrorPopover.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/ErrorPopover.service.js 
b/modules/web-console/frontend/app/services/ErrorPopover.service.js
index 5132d50..bddf436 100644
--- a/modules/web-console/frontend/app/services/ErrorPopover.service.js
+++ b/modules/web-console/frontend/app/services/ErrorPopover.service.js
@@ -19,19 +19,17 @@
  * Service to show/hide error popover.
  */
 export default class ErrorPopover {
-    static $inject = ['$popover', '$anchorScroll', '$location', '$timeout', 
'IgniteFormUtils'];
+    static $inject = ['$popover', '$anchorScroll', '$timeout', 
'IgniteFormUtils'];
 
     /**
      * @param $popover
      * @param $anchorScroll
-     * @param $location
      * @param $timeout
      * @param FormUtils
      */
-    constructor($popover, $anchorScroll, $location, $timeout, FormUtils) {
+    constructor($popover, $anchorScroll, $timeout, FormUtils) {
         this.$popover = $popover;
         this.$anchorScroll = $anchorScroll;
-        this.$location = $location;
         this.$timeout = $timeout;
         this.FormUtils = FormUtils;
 
@@ -73,11 +71,9 @@ export default class ErrorPopover {
             el = body.find('[name="' + id + '"]');
 
         if (el && el.length > 0) {
-            if (!ErrorPopover._isElementInViewport(el[0])) {
-                this.$location.hash(el[0].id);
+            if (!ErrorPopover._isElementInViewport(el[0]))
+                el[0].scrollIntoView();
 
-                this.$anchorScroll();
-            }
 
             const newPopover = this.$popover(el, {content: message});
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/services/FormUtils.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/FormUtils.service.js 
b/modules/web-console/frontend/app/services/FormUtils.service.js
index 6ccc3c6..f22d4bc 100644
--- a/modules/web-console/frontend/app/services/FormUtils.service.js
+++ b/modules/web-console/frontend/app/services/FormUtils.service.js
@@ -18,7 +18,7 @@
 export default ['IgniteFormUtils', ['$window', 'IgniteFocus', ($window, Focus) 
=> {
     function ensureActivePanel(ui, pnl, focusId) {
         if (ui && ui.loadPanel) {
-            const collapses = $('div.panel-collapse');
+            const collapses = $('[bs-collapse-target]');
 
             ui.loadPanel(pnl);
 
@@ -324,6 +324,22 @@ export default ['IgniteFormUtils', ['$window', 
'IgniteFocus', ($window, Focus) =
         return width | 0;
     }
 
+    // TODO: move somewhere else
+    function triggerValidation(form, $scope) {
+        const fe = (m) => Object.keys(m.$error)[0];
+        const em = (e) => (m) => {
+            if (!e) return;
+            const walk = (m) => {
+                if (!m.$error[e]) return;
+                if (m.$error[e] === true) return m;
+                return walk(m.$error[e][0]);
+            };
+            return walk(m);
+        };
+
+        $scope.$broadcast('$showValidationError', em(fe(form))(form));
+    }
+
     return {
         /**
          * Cut class name by width in pixel or width in symbol count.
@@ -434,6 +450,7 @@ export default ['IgniteFormUtils', ['$window', 
'IgniteFocus', ($window, Focus) =
         markPristineInvalidAsDirty(ngModelCtrl) {
             if (ngModelCtrl && ngModelCtrl.$invalid && ngModelCtrl.$pristine)
                 ngModelCtrl.$setDirty();
-        }
+        },
+        triggerValidation
     };
 }]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/services/IGFSs.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/IGFSs.js 
b/modules/web-console/frontend/app/services/IGFSs.js
new file mode 100644
index 0000000..87dfd17
--- /dev/null
+++ b/modules/web-console/frontend/app/services/IGFSs.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 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($http) {
+        Object.assign(this, {$http});
+    }
+
+    getIGFS(igfsID) {
+        return this.$http.get(`/api/v1/configuration/igfs/${igfsID}`);
+    }
+
+    getBlankIGFS() {
+        return {
+            _id: ObjectID.generate(),
+            ipcEndpointEnabled: true,
+            fragmentizerEnabled: true,
+            colocateMetadata: true,
+            relaxedConsistency: true
+        };
+    }
+
+    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']);
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/services/JavaTypes.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/JavaTypes.service.js 
b/modules/web-console/frontend/app/services/JavaTypes.service.js
index dff73a4..0e58b8d 100644
--- a/modules/web-console/frontend/app/services/JavaTypes.service.js
+++ b/modules/web-console/frontend/app/services/JavaTypes.service.js
@@ -15,6 +15,15 @@
  * limitations under the License.
  */
 
+import merge from 'lodash/merge';
+import uniq from 'lodash/uniq';
+import map from 'lodash/map';
+import reduce from 'lodash/reduce';
+import isObject from 'lodash/isObject';
+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';
 
@@ -46,8 +55,8 @@ export default class JavaTypes {
     static $inject = ['IgniteClusterDefaults', 'IgniteCacheDefaults', 
'IgniteIGFSDefaults'];
 
     constructor(clusterDflts, cacheDflts, igfsDflts) {
-        this.enumClasses = _.uniq(this._enumClassesAcc(_.merge(clusterDflts, 
cacheDflts, igfsDflts), []));
-        this.shortEnumClasses = _.map(this.enumClasses, (cls) => 
this.shortClassName(cls));
+        this.enumClasses = uniq(this._enumClassesAcc(merge(clusterDflts, 
cacheDflts, igfsDflts), []));
+        this.shortEnumClasses = map(this.enumClasses, (cls) => 
this.shortClassName(cls));
 
         JAVA_CLASS_STRINGS.push({short: 'byte[]', full: 'byte[]', stringValue: 
'[B'});
     }
@@ -61,10 +70,10 @@ export default class JavaTypes {
      * @private
      */
     _enumClassesAcc(root, classes) {
-        return _.reduce(root, (acc, val, key) => {
+        return reduce(root, (acc, val, key) => {
             if (key === 'clsName')
                 acc.push(val);
-            else if (_.isObject(val))
+            else if (isObject(val))
                 this._enumClassesAcc(val, acc);
 
             return acc;
@@ -78,7 +87,7 @@ export default class JavaTypes {
      * @return {boolean}
      */
     nonEnum(clsName) {
-        return !_.includes(this.shortEnumClasses, clsName) && 
!_.includes(this.enumClasses, clsName);
+        return !includes(this.shortEnumClasses, clsName) && 
!includes(this.enumClasses, clsName);
     }
 
     /**
@@ -86,7 +95,7 @@ export default class JavaTypes {
      * @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));
+        return isNil(find(JAVA_CLASSES, (clazz) => clsName === clazz.short || 
clsName === clazz.full));
     }
 
     /**
@@ -94,7 +103,7 @@ export default class JavaTypes {
      * @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);
+        const type = find(JAVA_CLASSES, (clazz) => clsName === clazz.short);
 
         return type ? type.full : clsName;
     }
@@ -166,7 +175,7 @@ export default class JavaTypes {
      * @returns {boolean} 'true' if given value is one of Java reserved 
keywords.
      */
     isKeyword(value) {
-        return !!(value && _.includes(JAVA_KEYWORDS, value.toLowerCase()));
+        return !!(value && includes(JAVA_KEYWORDS, value.toLowerCase()));
     }
 
     /**
@@ -174,7 +183,7 @@ export default class JavaTypes {
      * @returns {boolean} 'true' if given class name is java primitive.
      */
     isPrimitive(clsName) {
-        return _.includes(JAVA_PRIMITIVES, clsName);
+        return includes(JAVA_PRIMITIVES, clsName);
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/services/LegacyUtils.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/LegacyUtils.service.js 
b/modules/web-console/frontend/app/services/LegacyUtils.service.js
index b19bde3..8f283c0 100644
--- a/modules/web-console/frontend/app/services/LegacyUtils.service.js
+++ b/modules/web-console/frontend/app/services/LegacyUtils.service.js
@@ -295,6 +295,8 @@ export default ['IgniteLegacyUtils', ['IgniteErrorPopover', 
(ErrorPopover) => {
     }
 
     return {
+        VALID_JAVA_IDENTIFIER,
+        JAVA_KEYWORDS,
         mkOptions(options) {
             return _.map(options, (option) => {
                 return {value: option, label: isDefined(option) ? option : 
'Not set'};

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/services/Messages.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/Messages.service.js 
b/modules/web-console/frontend/app/services/Messages.service.js
index 39ffd3c..620d372 100644
--- a/modules/web-console/frontend/app/services/Messages.service.js
+++ b/modules/web-console/frontend/app/services/Messages.service.js
@@ -16,6 +16,8 @@
  */
 
 import {CancellationError} from 'app/errors/CancellationError';
+import isEmpty from 'lodash/isEmpty';
+import {nonEmpty} from 'app/utils/lodashMixins';
 
 // Service to show various information and error messages.
 export default ['IgniteMessages', ['$alert', ($alert) => {
@@ -37,8 +39,8 @@ export default ['IgniteMessages', ['$alert', ($alert) => {
                 return prefix + (errIndex >= 0 ? msg.substring(errIndex + 5, 
msg.length - 1) : msg);
             }
 
-            if (_.nonEmpty(err.className)) {
-                if (_.isEmpty(prefix))
+            if (nonEmpty(err.className)) {
+                if (isEmpty(prefix))
                     prefix = 'Internal cluster error: ';
 
                 return prefix + err.className;

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/services/Models.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/Models.js 
b/modules/web-console/frontend/app/services/Models.js
new file mode 100644
index 0000000..3b714c4
--- /dev/null
+++ b/modules/web-console/frontend/app/services/Models.js
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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';
+
+export default class Models {
+    static $inject = ['$http'];
+
+    /**
+     * @param {ng.IHttpService} $http
+     */
+    constructor($http) {
+        this.$http = $http;
+    }
+
+    /**
+     * @param {string} modelID
+     * @returns {ng.IPromise<ng.IHttpResponse<{data: 
ig.config.model.DomainModel}>>}
+     */
+    getModel(modelID) {
+        return this.$http.get(`/api/v1/configuration/domains/${modelID}`);
+    }
+
+    /**
+     * @returns {ig.config.model.DomainModel}
+     */
+    getBlankModel() {
+        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'}
+        ]
+    };
+
+    indexSortDirection = {
+        values: [
+            {value: true, label: 'ASC'},
+            {value: false, label: 'DESC'}
+        ],
+        default: true
+    };
+
+    normalize = omit(['__v', 'space']);
+
+    /**
+     * @param {Array<ig.config.model.IndexField>} fields
+     */
+    addIndexField(fields) {
+        return fields[fields.push({_id: ObjectID.generate(), direction: true}) 
- 1];
+    }
+
+    /**
+     * @param {ig.config.model.DomainModel} model
+     */
+    addIndex(model) {
+        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];
+    }
+
+    /**
+     * @param {ig.config.model.DomainModel} model
+     */
+    hasIndex(model) {
+        return model.queryMetadata === 'Configuration'
+            ? !!(model.keyFields && model.keyFields.length)
+            : (!model.generatePojo || !model.databaseSchema && 
!model.databaseTable);
+    }
+
+    /**
+     * @param {ig.config.model.DomainModel} model
+     * @returns {ig.config.model.ShortDomainModel}
+     */
+    toShortModel(model) {
+        return {
+            _id: model._id,
+            keyType: model.keyType,
+            valueType: model.valueType,
+            hasIndex: this.hasIndex(model)
+        };
+    }
+
+    queryIndexes = {
+        /**
+         * Validates query indexes for completeness
+         * @param {Array<ig.config.model.Index>} $value
+         */
+        complete: ($value = []) => $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
+         * @param {Array<ig.config.model.Index>} $value
+         * @param {Array<ig.config.model.Field>} fields
+         */
+        fieldsExist: ($value = [], fields = []) => {
+            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
+         * @param {Array<ig.config.model.Index>} $value
+         */
+        indexFieldsHaveUniqueNames: ($value = []) => {
+            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
+     * 
+     * @param {ig.config.model.DomainModel} model
+     * @returns {ig.config.model.DomainModel}
+     */
+    removeInvalidFields(model) {
+        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
+     * @param {Array<ig.config.model.KeyField|ig.config.model.ValueField>} 
DBFields
+     */
+    storeKeyDBFieldsUnique(DBFields = []) {
+        return ['databaseFieldName', 'javaFieldName'].every((key) => {
+            const items = new Set(DBFields.map((field) => field[key]));
+            return items.size === DBFields.length;
+        });
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/services/Version.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/Version.service.js 
b/modules/web-console/frontend/app/services/Version.service.js
index 6daf3aa..33de64d 100644
--- a/modules/web-console/frontend/app/services/Version.service.js
+++ b/modules/web-console/frontend/app/services/Version.service.js
@@ -16,6 +16,7 @@
  */
 
 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+import _ from 'lodash';
 
 /**
  * Utility service for version parsing and comparing

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/services/index.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/index.js 
b/modules/web-console/frontend/app/services/index.js
index 55f8d3d..77884df 100644
--- a/modules/web-console/frontend/app/services/index.js
+++ b/modules/web-console/frontend/app/services/index.js
@@ -16,10 +16,12 @@
  */
 
 import angular from 'angular';
+import Clusters from './Clusters';
 import IgniteVersion from './Version.service';
 import {default as DefaultState} from './DefaultState';
 
 export default angular
     .module('ignite-console.services', [])
+    .service('Clusters', Clusters)
     .provider('DefaultState', DefaultState)
     .service('IgniteVersion', IgniteVersion);

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/utils/lodashMixins.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/utils/lodashMixins.js 
b/modules/web-console/frontend/app/utils/lodashMixins.js
new file mode 100644
index 0000000..ff50ee0
--- /dev/null
+++ b/modules/web-console/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);

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/utils/uniqueName.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/utils/uniqueName.js 
b/modules/web-console/frontend/app/utils/uniqueName.js
new file mode 100644
index 0000000..bebe2c3
--- /dev/null
+++ b/modules/web-console/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;
+};

Reply via email to