http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/services/clusters.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/services/clusters.js 
b/modules/web-console/backend/services/clusters.js
index 49e6f09..0cc2b9f 100644
--- a/modules/web-console/backend/services/clusters.js
+++ b/modules/web-console/backend/services/clusters.js
@@ -23,16 +23,19 @@ const _ = require('lodash');
 
 module.exports = {
     implements: 'services/clusters',
-    inject: ['mongo', 'services/spaces', 'errors']
+    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, errors) => {
+module.exports.factory = (mongo, spacesService, cachesService, modelsService, 
igfssService, errors) => {
     /**
      * Convert remove status operation to own presentation.
      *
@@ -71,17 +74,17 @@ module.exports.factory = (mongo, spacesService, errors) => {
      */
     const create = (cluster) => {
         return mongo.Cluster.create(cluster)
-            .then((savedCluster) =>
-                mongo.Cache.update({_id: {$in: savedCluster.caches}}, 
{$addToSet: {clusters: savedCluster._id}}, {multi: true}).exec()
-                    .then(() => mongo.Igfs.update({_id: {$in: 
savedCluster.igfss}}, {$addToSet: {clusters: savedCluster._id}}, {multi: 
true}).exec())
-                    .then(() => savedCluster)
-            )
             .catch((err) => {
                 if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR)
-                    throw new errors.DuplicateKeyException('Cluster with name: 
"' + cluster.name + '" already exist.');
+                    throw new errors.DuplicateKeyException(`Cluster with name: 
"${cluster.name}" already exist.`);
                 else
                     throw err;
-            });
+            })
+            .then((savedCluster) =>
+                mongo.Cache.update({_id: {$in: savedCluster.caches}}, 
{$addToSet: {clusters: savedCluster._id}}, {multi: true}).exec()
+                    .then(() => mongo.Igfs.update({_id: {$in: 
savedCluster.igfss}}, {$addToSet: {clusters: savedCluster._id}}, {multi: 
true}).exec())
+                    .then(() => savedCluster)
+            );
     };
 
     /**
@@ -97,6 +100,110 @@ module.exports.factory = (mongo, spacesService, errors) => 
{
     };
 
     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.update(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.
          *
@@ -121,19 +228,31 @@ module.exports.factory = (mongo, spacesService, errors) 
=> {
         }
 
         /**
-         * Remove cluster.
+         * Remove clusters.
          *
-         * @param {mongo.ObjectId|String} clusterId - The cluster id for 
remove.
+         * @param {Array.<String>|String} ids - The cluster ids for remove.
          * @returns {Promise.<{rowsAffected}>} - The number of affected rows.
          */
-        static remove(clusterId) {
-            if (_.isNil(clusterId))
+        static remove(ids) {
+            if (_.isNil(ids))
                 return Promise.reject(new 
errors.IllegalArgumentException('Cluster id can not be undefined or null'));
 
-            return mongo.Cache.update({clusters: {$in: [clusterId]}}, {$pull: 
{clusters: clusterId}}, {multi: true}).exec()
-                .then(() => mongo.Igfs.update({clusters: {$in: [clusterId]}}, 
{$pull: {clusters: clusterId}}, {multi: true}).exec())
-                .then(() => mongo.Cluster.remove({_id: clusterId}).exec())
-                .then(convertRemoveStatus);
+            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) => {
+                        return Promise.all([
+                            mongo.DomainModel.remove({_id: {$in: 
cluster.models}}).exec(),
+                            mongo.Cache.remove({_id: {$in: 
cluster.caches}}).exec(),
+                            mongo.Igfs.remove({_id: {$in: 
cluster.igfss}}).exec()
+                        ]);
+                    });
+            }))
+                .then(() => ({rowsAffected: ids.length}));
         }
 
         /**

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/services/configurations.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/services/configurations.js 
b/modules/web-console/backend/services/configurations.js
index 36d9932..da431c3 100644
--- a/modules/web-console/backend/services/configurations.js
+++ b/modules/web-console/backend/services/configurations.js
@@ -52,6 +52,18 @@ module.exports.factory = (mongo, spacesService, 
clustersService, cachesService,
                 ]))
                 .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;

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/services/domains.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/services/domains.js 
b/modules/web-console/backend/services/domains.js
index 986991d..ba3a6a5 100644
--- a/modules/web-console/backend/services/domains.js
+++ b/modules/web-console/backend/services/domains.js
@@ -149,6 +149,87 @@ module.exports.factory = (mongo, spacesService, 
cachesService, errors) => {
     };
 
     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.update(query, {$set: model}, {upsert: 
true}).exec()
+                .then(() => mongo.Cache.update({_id: {$in: model.caches}}, 
{$addToSet: {domains: model._id}}, {multi: true}).exec())
+                .then(() => mongo.Cache.update({_id: {$nin: model.caches}}, 
{$pull: {domains: model._id}}, {multi: true}).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.update({domains: {$in: ids}}, {$pull: {domains: 
ids}}, {multi: true}).exec()
+                .then(() => mongo.Cluster.update({models: {$in: ids}}, {$pull: 
{models: ids}}, {multi: true}).exec())
+                .then(() => mongo.DomainModel.remove({_id: {$in: ids}}).exec())
+                .then(convertRemoveStatus);
+        }
+
         /**
          * Batch merging domains.
          *
@@ -169,21 +250,6 @@ module.exports.factory = (mongo, spacesService, 
cachesService, errors) => {
         }
 
         /**
-         * Remove domain.
-         *
-         * @param {mongo.ObjectId|String} domainId - The domain id for remove.
-         * @returns {Promise.<{rowsAffected}>} - The number of affected rows.
-         */
-        static remove(domainId) {
-            if (_.isNil(domainId))
-                return Promise.reject(new 
errors.IllegalArgumentException('Domain id can not be undefined or null'));
-
-            return mongo.Cache.update({domains: {$in: [domainId]}}, {$pull: 
{domains: domainId}}, {multi: true}).exec()
-                .then(() => mongo.DomainModel.remove({_id: domainId}).exec())
-                .then(convertRemoveStatus);
-        }
-
-        /**
          * 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.

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/services/igfss.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/services/igfss.js 
b/modules/web-console/backend/services/igfss.js
index 5296f16..b75d677 100644
--- a/modules/web-console/backend/services/igfss.js
+++ b/modules/web-console/backend/services/igfss.js
@@ -93,6 +93,31 @@ module.exports.factory = (mongo, spacesService, errors) => {
     };
 
     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.update(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.
          *
@@ -117,22 +142,27 @@ module.exports.factory = (mongo, spacesService, errors) 
=> {
         }
 
         /**
-         * Remove IGFS.
+         * Remove IGFSs.
          *
-         * @param {mongo.ObjectId|String} igfsId - The IGFS id for remove.
+         * @param {Array.<String>|String} ids - The IGFS ids for remove.
          * @returns {Promise.<{rowsAffected}>} - The number of affected rows.
          */
-        static remove(igfsId) {
-            if (_.isNil(igfsId))
+        static remove(ids) {
+            if (_.isNil(ids))
                 return Promise.reject(new 
errors.IllegalArgumentException('IGFS id can not be undefined or null'));
 
-            return mongo.Cluster.update({igfss: {$in: [igfsId]}}, {$pull: 
{igfss: igfsId}}, {multi: true}).exec()
-                // TODO WC-201 fix clenup on node filter on deletion for 
cluster serviceConfigurations and caches.
+            ids = _.castArray(ids);
+
+            if (_.isEmpty(ids))
+                return Promise.resolve({rowsAffected: 0});
+
+            return mongo.Cluster.update({igfss: {$in: ids}}, {$pull: {igfss: 
{$in: ids}}}, {multi: true}).exec()
+                // TODO WC-201 fix cleanup on node filter on deletion for 
cluster serviceConfigurations and caches.
                 // .then(() => mongo.Cluster.update({ 
'serviceConfigurations.$.nodeFilter.kind': { $ne: 'IGFS' }, 
'serviceConfigurations.nodeFilter.IGFS.igfs': igfsId},
                 //     {$unset: 
{'serviceConfigurations.$.nodeFilter.IGFS.igfs': ''}}, {multi: true}).exec())
                 // .then(() => mongo.Cluster.update({ 
'serviceConfigurations.nodeFilter.kind': 'IGFS', 
'serviceConfigurations.nodeFilter.IGFS.igfs': igfsId},
                 //     {$unset: {'serviceConfigurations.$.nodeFilter': ''}}, 
{multi: true}).exec())
-                .then(() => mongo.Igfs.remove({_id: igfsId}).exec())
+                .then(() => mongo.Igfs.remove({_id: {$in: ids}}).exec())
                 .then(convertRemoveStatus);
         }
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/services/sessions.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/services/sessions.js 
b/modules/web-console/backend/services/sessions.js
index 0518ce2..0ea851b 100644
--- a/modules/web-console/backend/services/sessions.js
+++ b/modules/web-console/backend/services/sessions.js
@@ -51,7 +51,7 @@ module.exports.factory = (mongo, errors) => {
             return new Promise((resolve) => {
                 delete session.viewedUser;
 
-                resolve();
+                resolve(true);
             });
         }
     }

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/services/spaces.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/services/spaces.js 
b/modules/web-console/backend/services/spaces.js
index 85f346e..fe62f77 100644
--- a/modules/web-console/backend/services/spaces.js
+++ b/modules/web-console/backend/services/spaces.js
@@ -57,7 +57,7 @@ module.exports.factory = (mongo, errors) => {
          */
         static spaceIds(userId, demo) {
             return this.spaces(userId, demo)
-                .then((spaces) => spaces.map((space) => space._id));
+                .then((spaces) => spaces.map((space) => space._id.toString()));
         }
 
         /**

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/test/unit/CacheService.test.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/test/unit/CacheService.test.js 
b/modules/web-console/backend/test/unit/CacheService.test.js
index 304f62c..52936a0 100644
--- a/modules/web-console/backend/test/unit/CacheService.test.js
+++ b/modules/web-console/backend/test/unit/CacheService.test.js
@@ -21,7 +21,7 @@ const testCaches = require('../data/caches.json');
 const testAccounts = require('../data/accounts.json');
 const testSpaces = require('../data/spaces.json');
 
-let cacheService;
+let cachesService;
 let mongo;
 let errors;
 let db;
@@ -34,7 +34,7 @@ suite('CacheServiceTestsSuite', () => {
             injector('dbHelper')])
             .then(([_cacheService, _mongo, _errors, _db]) => {
                 mongo = _mongo;
-                cacheService = _cacheService;
+                cachesService = _cacheService;
                 errors = _errors;
                 db = _db;
             });
@@ -42,12 +42,24 @@ suite('CacheServiceTestsSuite', () => {
 
     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;
 
-        cacheService.merge(dupleCache)
+        cachesService.merge(dupleCache)
             .then((cache) => mongo.Cache.findById(cache._id))
             .then((cache) => assert.isNotNull(cache))
             .then(done)
@@ -59,7 +71,7 @@ suite('CacheServiceTestsSuite', () => {
 
         const cacheBeforeMerge = Object.assign({}, testCaches[0], {name: 
newName});
 
-        cacheService.merge(cacheBeforeMerge)
+        cachesService.merge(cacheBeforeMerge)
             .then((cache) => mongo.Cache.findById(cache._id))
             .then((cacheAfterMerge) => assert.equal(cacheAfterMerge.name, 
newName))
             .then(done)
@@ -71,7 +83,7 @@ suite('CacheServiceTestsSuite', () => {
 
         delete dupleCache._id;
 
-        cacheService.merge(dupleCache)
+        cachesService.merge(dupleCache)
             .catch((err) => {
                 assert.instanceOf(err, errors.DuplicateKeyException);
 
@@ -80,7 +92,7 @@ suite('CacheServiceTestsSuite', () => {
     });
 
     test('Remove existed cache', (done) => {
-        cacheService.remove(testCaches[0]._id)
+        cachesService.remove(testCaches[0]._id)
             .then(({rowsAffected}) =>
                 assert.equal(rowsAffected, 1)
             )
@@ -93,7 +105,7 @@ suite('CacheServiceTestsSuite', () => {
     });
 
     test('Remove cache without identifier', (done) => {
-        cacheService.remove()
+        cachesService.remove()
             .catch((err) => {
                 assert.instanceOf(err, errors.IllegalArgumentException);
 
@@ -104,7 +116,7 @@ suite('CacheServiceTestsSuite', () => {
     test('Remove missed cache', (done) => {
         const validNoExistingId = 'FFFFFFFFFFFFFFFFFFFFFFFF';
 
-        cacheService.remove(validNoExistingId)
+        cachesService.remove(validNoExistingId)
             .then(({rowsAffected}) =>
                 assert.equal(rowsAffected, 0)
             )
@@ -113,7 +125,7 @@ suite('CacheServiceTestsSuite', () => {
     });
 
     test('Get all caches by space', (done) => {
-        cacheService.listBySpaces(testSpaces[0]._id)
+        cachesService.listBySpaces(testSpaces[0]._id)
             .then((caches) =>
                 assert.equal(caches.length, 5)
             )
@@ -122,7 +134,7 @@ suite('CacheServiceTestsSuite', () => {
     });
 
     test('Remove all caches in space', (done) => {
-        cacheService.removeAll(testAccounts[0]._id, false)
+        cachesService.removeAll(testAccounts[0]._id, false)
             .then(({rowsAffected}) =>
                 assert.equal(rowsAffected, 5)
             )
@@ -130,6 +142,19 @@ suite('CacheServiceTestsSuite', () => {
             .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();

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/test/unit/ClusterService.test.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/test/unit/ClusterService.test.js 
b/modules/web-console/backend/test/unit/ClusterService.test.js
index ed04c45..66c7cf1 100644
--- a/modules/web-console/backend/test/unit/ClusterService.test.js
+++ b/modules/web-console/backend/test/unit/ClusterService.test.js
@@ -15,13 +15,17 @@
  * 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;
@@ -29,12 +33,14 @@ let db;
 suite('ClusterServiceTestsSuite', () => {
     suiteSetup(() => {
         return Promise.all([injector('services/clusters'),
+            injector('services/caches'),
             injector('mongo'),
             injector('errors'),
             injector('dbHelper')])
-            .then(([_clusterService, _mongo, _errors, _db]) => {
+            .then(([_clusterService, _cacheService, _mongo, _errors, _db]) => {
                 mongo = _mongo;
                 clusterService = _clusterService;
+                cacheService = _cacheService;
                 errors = _errors;
                 db = _db;
             });
@@ -42,6 +48,18 @@ suite('ClusterServiceTestsSuite', () => {
 
     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'});
 
@@ -130,6 +148,219 @@ suite('ClusterServiceTestsSuite', () => {
             .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-caches');
+                assert.isNotNull(clusters[0].discovery);
+                assert.equal(clusters[0].cachesCount, 5);
+                assert.equal(clusters[0].modelsCount, 5);
+                assert.equal(clusters[0].igfsCount, 0);
+
+                assert.equal(clusters[1].name, 'cluster-igfs');
+                assert.isNotNull(clusters[1].discovery);
+                assert.equal(clusters[1].cachesCount, 2);
+                assert.equal(clusters[1].modelsCount, 5);
+                assert.equal(clusters[1].igfsCount, 1);
+            })
+            .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.n, 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.isNotNull(c2);
+                assert.equal(c2.cacheMode, 'PARTITIONED');
+                assert.isTrue(c2.readThrough);
+            })
+            .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();

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/test/unit/DomainService.test.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/test/unit/DomainService.test.js 
b/modules/web-console/backend/test/unit/DomainService.test.js
index c7cf149..e4c531d 100644
--- a/modules/web-console/backend/test/unit/DomainService.test.js
+++ b/modules/web-console/backend/test/unit/DomainService.test.js
@@ -150,6 +150,11 @@ suite('DomainsServiceTestsSuite', () => {
             .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();

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/components/ListEditable.js
----------------------------------------------------------------------
diff --git a/modules/web-console/e2e/testcafe/components/ListEditable.js 
b/modules/web-console/e2e/testcafe/components/ListEditable.js
new file mode 100644
index 0000000..acce0c6
--- /dev/null
+++ b/modules/web-console/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)
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/components/Table.js
----------------------------------------------------------------------
diff --git a/modules/web-console/e2e/testcafe/components/Table.js 
b/modules/web-console/e2e/testcafe/components/Table.js
new file mode 100644
index 0000000..e690599
--- /dev/null
+++ b/modules/web-console/e2e/testcafe/components/Table.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 {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 {
+    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"]')
+    }
+
+    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`))
+    }
+
+    findCell(rowIndex, columnLabel) {
+        return Selector(findCell(this._selector, rowIndex, columnLabel))
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/components/pageAdvancedConfiguration.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/e2e/testcafe/components/pageAdvancedConfiguration.js 
b/modules/web-console/e2e/testcafe/components/pageAdvancedConfiguration.js
new file mode 100644
index 0000000..eabd337
--- /dev/null
+++ b/modules/web-console/e2e/testcafe/components/pageAdvancedConfiguration.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'
+
+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)
+    }
+};
+
+export class Panel {
+    constructor(title) {
+        this._selector = 
Selector('.pca-panel-heading-title').withText(title).parent('.pca-panel');
+        this.heading = this._selector.find('.pca-panel-heading')
+        this.body = 
this._selector.find('.pca-panel-collapse').addCustomDOMProperties({
+            isOpened: el => el.classList.contains('in')
+        })
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/components/pageConfiguration.js
----------------------------------------------------------------------
diff --git a/modules/web-console/e2e/testcafe/components/pageConfiguration.js 
b/modules/web-console/e2e/testcafe/components/pageConfiguration.js
new file mode 100644
index 0000000..c364208
--- /dev/null
+++ b/modules/web-console/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"]');

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/fixtures/configuration/basic.js
----------------------------------------------------------------------
diff --git a/modules/web-console/e2e/testcafe/fixtures/configuration/basic.js 
b/modules/web-console/e2e/testcafe/fixtures/configuration/basic.js
new file mode 100644
index 0000000..090fd0a
--- /dev/null
+++ b/modules/web-console/e2e/testcafe/fixtures/configuration/basic.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 {Selector, Role} from 'testcafe';
+import {dropTestDB, insertTestUser, resolveUrl} from '../../envtools';
+import {createRegularUser} from '../../roles';
+import {PageConfigurationBasic} from 
'../../page-models/PageConfigurationBasic';
+import {successNotification} from '../../components/notifications';
+
+const regularUser = createRegularUser();
+
+fixture('Basic configuration')
+    .before(async(t) => {
+        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')
+        .expect(page.buttonDownloadProject.visible).notOk('Download 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');
+
+    // TODO IGNITE-8094: restore to save method call.
+    await page.saveWithoutDownload();
+    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}’`);
+});

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/fixtures/configuration/overview.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/e2e/testcafe/fixtures/configuration/overview.js 
b/modules/web-console/e2e/testcafe/fixtures/configuration/overview.js
new file mode 100644
index 0000000..f0495bd
--- /dev/null
+++ b/modules/web-console/e2e/testcafe/fixtures/configuration/overview.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 {Selector} from 'testcafe';
+import {getLocationPathname} from '../../helpers';
+import {dropTestDB, insertTestUser, resolveUrl} from '../../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);
+});

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/fixtures/menu-smoke.js
----------------------------------------------------------------------
diff --git a/modules/web-console/e2e/testcafe/fixtures/menu-smoke.js 
b/modules/web-console/e2e/testcafe/fixtures/menu-smoke.js
index fea019b..fdd0edb 100644
--- a/modules/web-console/e2e/testcafe/fixtures/menu-smoke.js
+++ b/modules/web-console/e2e/testcafe/fixtures/menu-smoke.js
@@ -39,7 +39,7 @@ test('Ingite main menu smoke test', async(t) => {
     await t
         .click(configureNavButton)
         .expect(Selector('title').innerText)
-        .eql('Basic Configuration – Apache Ignite Web Console');
+        .eql('Configuration – Apache Ignite Web Console');
 
     await t
         .click(queriesNavButton)

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/package.json
----------------------------------------------------------------------
diff --git a/modules/web-console/e2e/testcafe/package.json 
b/modules/web-console/e2e/testcafe/package.json
index a6e5b0d..2501102 100644
--- a/modules/web-console/e2e/testcafe/package.json
+++ b/modules/web-console/e2e/testcafe/package.json
@@ -39,7 +39,7 @@
     "objectid": "3.2.1",
     "path": "0.12.7",
     "sinon": "2.3.8",
-    "testcafe": "0.18.5",
+    "testcafe": "^0.19.0",
     "testcafe-angular-selectors": "0.3.0",
     "testcafe-reporter-teamcity": "1.0.9",
     "type-detect": "4.0.3",

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/page-models/PageConfigurationAdvancedCluster.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/e2e/testcafe/page-models/PageConfigurationAdvancedCluster.js
 
b/modules/web-console/e2e/testcafe/page-models/PageConfigurationAdvancedCluster.js
new file mode 100644
index 0000000..0f62707
--- /dev/null
+++ 
b/modules/web-console/e2e/testcafe/page-models/PageConfigurationAdvancedCluster.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, t} from 'testcafe'
+
+export class PageConfigurationAdvancedCluster {
+    constructor() {
+        this._selector = Selector('page-configure-advanced-cluster')
+        this.saveButton = Selector('.pc-form-actions-panel 
.btn-ignite').withText('Save')
+    }
+    async save() {
+        await t.click(this.saveButton)
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/page-models/PageConfigurationBasic.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/e2e/testcafe/page-models/PageConfigurationBasic.js 
b/modules/web-console/e2e/testcafe/page-models/PageConfigurationBasic.js
new file mode 100644
index 0000000..38610bc
--- /dev/null
+++ b/modules/web-console/e2e/testcafe/page-models/PageConfigurationBasic.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 {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 changes and download 
project';
+    static SAVE_CHANGES_LABEL = 'Save changes';
+
+    constructor() {
+        this._selector = Selector('page-configure-basic');
+        this.versionPicker = new VersionPicker;
+        this.totalOffheapSizeInput = Selector('pc-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.buttonDownloadProject = Selector('button-download-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('.pc-page-header')
+    }
+
+    async save() {
+        await t.click(this.mainFormAction)
+    }
+
+    async saveWithoutDownload() {
+        return await 
t.click(this.contextFormActionsButton).click(this.contextSaveButton)
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/page-models/PageConfigurationOverview.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/e2e/testcafe/page-models/PageConfigurationOverview.js 
b/modules/web-console/e2e/testcafe/page-models/PageConfigurationOverview.js
new file mode 100644
index 0000000..34a6486
--- /dev/null
+++ b/modules/web-console/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();
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/page-models/pageConfigurationAdvancedIGFS.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/e2e/testcafe/page-models/pageConfigurationAdvancedIGFS.js 
b/modules/web-console/e2e/testcafe/page-models/pageConfigurationAdvancedIGFS.js
new file mode 100644
index 0000000..f3ac35c
--- /dev/null
+++ 
b/modules/web-console/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);

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/page-models/pageConfigurationAdvancedModels.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/e2e/testcafe/page-models/pageConfigurationAdvancedModels.js
 
b/modules/web-console/e2e/testcafe/page-models/pageConfigurationAdvancedModels.js
new file mode 100644
index 0000000..196ac3c
--- /dev/null
+++ 
b/modules/web-console/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'})
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/roles.js
----------------------------------------------------------------------
diff --git a/modules/web-console/e2e/testcafe/roles.js 
b/modules/web-console/e2e/testcafe/roles.js
index 99a4d31..5f584b2 100644
--- a/modules/web-console/e2e/testcafe/roles.js
+++ b/modules/web-console/e2e/testcafe/roles.js
@@ -27,7 +27,6 @@ export const createRegularUser = () => {
         await t.eval(() => window.localStorage.showGettingStarted = 'false');
 
         const page = new PageSignIn();
-        await page.open();
         await page.login('a@a', 'a');
     });
 };

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/.babelrc
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/.babelrc 
b/modules/web-console/frontend/.babelrc
index da16f08..1759c44 100644
--- a/modules/web-console/frontend/.babelrc
+++ b/modules/web-console/frontend/.babelrc
@@ -1,4 +1,4 @@
 {
   "presets": ["es2015", "stage-1"],
-  "plugins": ["add-module-exports"]
+  "plugins": ["add-module-exports", "transform-object-rest-spread"]
 }

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/.eslintrc
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/.eslintrc 
b/modules/web-console/frontend/.eslintrc
index 3c26fa7..75de1ea 100644
--- a/modules/web-console/frontend/.eslintrc
+++ b/modules/web-console/frontend/.eslintrc
@@ -159,7 +159,7 @@ rules:
     no-unneeded-ternary: 2
     no-unreachable: 2
     no-unused-expressions: [2, { allowShortCircuit: true }]
-    no-unused-vars: [2, {"vars": "all", "args": "after-used"}]
+    no-unused-vars: [0, {"vars": "all", "args": "after-used"}]
     no-use-before-define: 2
     no-useless-call: 2
     no-void: 0

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/.gitignore
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/.gitignore 
b/modules/web-console/frontend/.gitignore
index 4fc11f46..60d2029 100644
--- a/modules/web-console/frontend/.gitignore
+++ b/modules/web-console/frontend/.gitignore
@@ -1,3 +1,8 @@
+*.idea
+*.log
 *.log.*
+.npmrc
+build/*
+node_modules
 public/stylesheets/*.css
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/app.config.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/app.config.js 
b/modules/web-console/frontend/app/app.config.js
index 6ba1d98..aa604af 100644
--- a/modules/web-console/frontend/app/app.config.js
+++ b/modules/web-console/frontend/app/app.config.js
@@ -17,12 +17,16 @@
 
 import _ from 'lodash';
 import angular from 'angular';
+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);
+const nonNil = negate(isNil);
+const nonEmpty = negate(isEmpty);
 const id8 = (uuid) => uuid.substring(0, 8).toUpperCase();
 
-_.mixin({
+mixin({
     nonNil,
     nonEmpty,
     id8
@@ -36,7 +40,7 @@ const igniteConsoleCfg = 
angular.module('ignite-console.config', ['ngAnimate', '
 
 // Configure AngularJS animation: do not animate fa-spin.
 igniteConsoleCfg.config(['$animateProvider', ($animateProvider) => {
-    $animateProvider.classNameFilter(/^((?!(fa-spin)).)*$/);
+    
$animateProvider.classNameFilter(/^((?!(fa-spin|ng-animate-disabled)).)*$/);
 }]);
 
 // AngularStrap modal popup configuration.
@@ -115,3 +119,20 @@ igniteConsoleCfg.config(['$datepickerProvider', 
($datepickerProvider) => {
 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}});
+        }
+    };
+});
+

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/app.d.ts
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/app.d.ts 
b/modules/web-console/frontend/app/app.d.ts
new file mode 100644
index 0000000..69cc7ab
--- /dev/null
+++ b/modules/web-console/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;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/app.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/app.js 
b/modules/web-console/frontend/app/app.js
index d01d9aa..871b06f 100644
--- a/modules/web-console/frontend/app/app.js
+++ b/modules/web-console/frontend/app/app.js
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 
+import './vendor';
 import '../public/stylesheets/style.scss';
 import '../app/primitives';
 
@@ -41,6 +42,7 @@ import './modules/dialog/dialog.module';
 import './modules/ace.module';
 import './modules/socket.module';
 import './modules/loading/loading.module';
+import servicesModule from './services';
 // endignite
 
 // Data
@@ -67,10 +69,11 @@ import igniteUiAceDocker from 
'./directives/ui-ace-docker/ui-ace-docker.directiv
 import igniteUiAceTabs from './directives/ui-ace-tabs.directive';
 import igniteRetainSelection from './directives/retain-selection.directive';
 import btnIgniteLink from './directives/btn-ignite-link';
+import exposeInput from './components/expose-ignite-form-field-control';
 
 // Services.
 import ChartColors from './services/ChartColors.service';
-import Confirm from './services/Confirm.service.js';
+import {default as IgniteConfirm, Confirm} from 
'./services/Confirm.service.js';
 import ConfirmBatch from './services/ConfirmBatch.service.js';
 import CopyToClipboard from './services/CopyToClipboard.service';
 import Countries from './services/Countries.service';
@@ -85,10 +88,11 @@ import LegacyUtils from './services/LegacyUtils.service';
 import Messages from './services/Messages.service';
 import ModelNormalizer from './services/ModelNormalizer.service.js';
 import UnsavedChangesGuard from './services/UnsavedChangesGuard.service';
-import Clusters from './services/Clusters';
 import Caches from './services/Caches';
 import {CSV} from './services/CSV';
 import {$exceptionHandler} from './services/exceptionHandler.js';
+import IGFSs from './services/IGFSs';
+import Models from './services/Models';
 
 import AngularStrapTooltip from './services/AngularStrapTooltip.decorator';
 import AngularStrapSelect from './services/AngularStrapSelect.decorator';
@@ -119,6 +123,7 @@ import pageConfigure from './components/page-configure';
 import pageConfigureBasic from './components/page-configure-basic';
 import pageConfigureAdvanced from './components/page-configure-advanced';
 import pageQueries from './components/page-queries';
+import pageConfigureOverview from './components/page-configure-overview';
 import gridColumnSelector from './components/grid-column-selector';
 import gridItemSelected from './components/grid-item-selected';
 import gridNoData from './components/grid-no-data';
@@ -209,6 +214,7 @@ angular.module('ignite-console', [
     pageConfigureBasic.name,
     pageConfigureAdvanced.name,
     pageQueries.name,
+    pageConfigureOverview.name,
     gridColumnSelector.name,
     gridItemSelected.name,
     gridNoData.name,
@@ -221,9 +227,11 @@ angular.module('ignite-console', [
     AngularStrapSelect.name,
     listEditable.name,
     clusterSelector.name,
+    servicesModule.name,
     connectedClusters.name,
     igniteListOfRegisteredUsers.name,
     pageProfile.name,
+    exposeInput.name,
     pageSignIn.name,
     pageLanding.name,
     pagePasswordChanged.name,
@@ -262,8 +270,9 @@ angular.module('ignite-console', [
 .service('JavaTypes', JavaTypes)
 .service('SqlTypes', SqlTypes)
 .service(...ChartColors)
-.service(...Confirm)
-.service(...ConfirmBatch)
+.service(...IgniteConfirm)
+.service(Confirm.name, Confirm)
+.service('IgniteConfirmBatch', ConfirmBatch)
 .service(...CopyToClipboard)
 .service(...Countries)
 .service(...Focus)
@@ -275,9 +284,10 @@ angular.module('ignite-console', [
 .service(...LegacyUtils)
 .service(...UnsavedChangesGuard)
 .service('IgniteActivitiesUserDialog', IgniteActivitiesUserDialog)
-.service('Clusters', Clusters)
 .service('Caches', Caches)
 .service(CSV.name, CSV)
+.service('IGFSs', IGFSs)
+.service('Models', Models)
 // Controllers.
 .controller(...resetPassword)
 // Filters.
@@ -346,11 +356,7 @@ angular.module('ignite-console', [
         $root.revertIdentity = () => {
             $http.get('/api/v1/admin/revert/identity')
                 .then(() => User.load())
-                .then((user) => {
-                    $root.$broadcast('user', user);
-
-                    $state.go('base.settings.admin');
-                })
+                .then(() => $state.go('base.settings.admin'))
                 .then(() => Notebook.load())
                 .catch(Messages.showError);
         };

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/bs-select-menu/style.scss
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/bs-select-menu/style.scss 
b/modules/web-console/frontend/app/components/bs-select-menu/style.scss
index d82bf19..4c071f6 100644
--- a/modules/web-console/frontend/app/components/bs-select-menu/style.scss
+++ b/modules/web-console/frontend/app/components/bs-select-menu/style.scss
@@ -84,13 +84,13 @@
         z-index: -1;
     }
     
-    &.bssm-multiple {
+    [class*='bssm-multiple'] {
         .bssm-active-indicator {
             display: initial;
         }
     }
 
-    &:not(.bssm-multiple) {
+    &:not([class*='bssm-multiple']) {
         .bssm-active-indicator {
             display: none;
         }

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/expose-ignite-form-field-control/directives.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/expose-ignite-form-field-control/directives.js
 
b/modules/web-console/frontend/app/components/expose-ignite-form-field-control/directives.js
new file mode 100644
index 0000000..5184032
--- /dev/null
+++ 
b/modules/web-console/frontend/app/components/expose-ignite-form-field-control/directives.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.
+ */
+
+// eslint-disable-next-line
+import {IgniteFormField} from 
'app/components/page-configure/components/pcValidation'
+
+/**
+ * Exposes input to .ignite-form-field scope
+ */
+class ExposeIgniteFormFieldControl {
+    /** @type {IgniteFormField} */
+    formField;
+    /** @type {ng.INgModelController} */
+    ngModel;
+    /** 
+     * Name used to access control from $scope.
+     * @type {string}
+     */
+    name;
+
+    $onInit() {
+        if (this.formField && this.ngModel) 
this.formField.exposeControl(this.ngModel, this.name);
+    }
+}
+
+export function exposeIgniteFormFieldControl() {
+    return {
+        restrict: 'A',
+        controller: ExposeIgniteFormFieldControl,
+        bindToController: {
+            name: '@exposeIgniteFormFieldControl'
+        },
+        require: {
+            formField: '^^?igniteFormField',
+            ngModel: '?ngModel'
+        },
+        scope: false
+    };
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/expose-ignite-form-field-control/index.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/expose-ignite-form-field-control/index.js
 
b/modules/web-console/frontend/app/components/expose-ignite-form-field-control/index.js
new file mode 100644
index 0000000..9a22478
--- /dev/null
+++ 
b/modules/web-console/frontend/app/components/expose-ignite-form-field-control/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 {igniteFormField, exposeIgniteFormFieldControl} from './directives';
+
+export default angular
+.module('expose-ignite-form-field-control', [])
+.directive('exposeIgniteFormFieldControl', exposeIgniteFormFieldControl);

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/grid-column-selector/template.pug
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/grid-column-selector/template.pug 
b/modules/web-console/frontend/app/components/grid-column-selector/template.pug
index 86fd152..afb246e 100644
--- 
a/modules/web-console/frontend/app/components/grid-column-selector/template.pug
+++ 
b/modules/web-console/frontend/app/components/grid-column-selector/template.pug
@@ -24,5 +24,6 @@ button.btn-ignite.btn-ignite--link-dashed-secondary(
     bs-on-before-show='$ctrl.onShow'
     data-multiple='true'
     ng-transclude
+    ng-show='$ctrl.columnsMenu.length'
 )
     svg(ignite-icon='gear').icon

Reply via email to