You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ak...@apache.org on 2015/07/10 20:38:03 UTC

incubator-ignite git commit: IGNITE-843: Added showInfo() and showError() common functions. WIP on cache type metadata.

Repository: incubator-ignite
Updated Branches:
  refs/heads/ignite-843 44a028ca0 -> 5e41cfa5e


IGNITE-843: Added showInfo() and showError() common functions. WIP on cache type metadata.


Project: http://git-wip-us.apache.org/repos/asf/incubator-ignite/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-ignite/commit/5e41cfa5
Tree: http://git-wip-us.apache.org/repos/asf/incubator-ignite/tree/5e41cfa5
Diff: http://git-wip-us.apache.org/repos/asf/incubator-ignite/diff/5e41cfa5

Branch: refs/heads/ignite-843
Commit: 5e41cfa5ee407f40d0f1fa63c692515adb844811
Parents: 44a028c
Author: AKuznetsov <ak...@gridgain.com>
Authored: Sat Jul 11 01:37:52 2015 +0700
Committer: AKuznetsov <ak...@gridgain.com>
Committed: Sat Jul 11 01:37:52 2015 +0700

----------------------------------------------------------------------
 modules/web-control-center/nodejs/app.js        |   6 +-
 .../nodejs/controllers/admin-controller.js      |  31 +-
 .../controllers/cache-viewer-controller.js      |   3 +-
 .../nodejs/controllers/caches-controller.js     |  77 +----
 .../nodejs/controllers/clusters-controller.js   |  21 +-
 .../nodejs/controllers/common-module.js         |  29 +-
 .../nodejs/controllers/metadata-controller.js   | 170 ++++++++++
 .../nodejs/controllers/models/metadata.json     |  11 +
 .../controllers/persistence-controller.js       | 213 +++++++++++++
 .../controllers/persistences-controller.js      | 214 -------------
 .../nodejs/controllers/profile-controller.js    |  27 +-
 modules/web-control-center/nodejs/db.js         |   8 +
 .../nodejs/routes/metadata.js                   |  89 ++++++
 .../nodejs/routes/persistence.js                | 311 +++++++++++++++++++
 .../nodejs/routes/persistences.js               | 311 -------------------
 .../nodejs/tests/routes/persistence.js          |   2 +-
 .../nodejs/views/configuration/metadata.jade    |  42 +++
 .../nodejs/views/configuration/persistence.jade |   2 +-
 .../nodejs/views/templates/layout-sidebar.jade  |  43 ++-
 19 files changed, 920 insertions(+), 690 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/app.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/app.js b/modules/web-control-center/nodejs/app.js
index 175ff00..79e51c7 100644
--- a/modules/web-control-center/nodejs/app.js
+++ b/modules/web-control-center/nodejs/app.js
@@ -28,7 +28,8 @@ var mongoStore = require('connect-mongo')(session);
 var publicRoutes = require('./routes/public');
 var clustersRouter = require('./routes/clusters');
 var cachesRouter = require('./routes/caches');
-var persistencesRouter = require('./routes/persistences');
+var metadataRouter = require('./routes/metadata');
+var persistenceRouter = require('./routes/persistence');
 var summary = require('./routes/summary');
 var adminRouter = require('./routes/admin');
 var profileRouter = require('./routes/profile');
@@ -131,7 +132,8 @@ app.use('/profile', mustAuthenticated, profileRouter);
 
 app.use('/configuration/clusters', clustersRouter);
 app.use('/configuration/caches', cachesRouter);
-app.use('/configuration/persistences', persistencesRouter);
+app.use('/configuration/metadata', metadataRouter);
+app.use('/configuration/persistence', persistenceRouter);
 app.use('/configuration/summary', summary);
 app.use('/sql', sqlRouter);
 

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/controllers/admin-controller.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/controllers/admin-controller.js b/modules/web-control-center/nodejs/controllers/admin-controller.js
index 870821d..2d85208 100644
--- a/modules/web-control-center/nodejs/controllers/admin-controller.js
+++ b/modules/web-control-center/nodejs/controllers/admin-controller.js
@@ -15,35 +15,16 @@
  * limitations under the License.
  */
 
-controlCenterModule.controller('adminController', ['$scope', '$alert', '$http', 'commonFunctions', function ($scope, $alert, $http, commonFunctions) {
+controlCenterModule.controller('adminController', ['$scope', '$http', 'commonFunctions', function ($scope, $http, commonFunctions) {
         $scope.userList = null;
 
-        $scope.showInfo = function (msg) {
-            $scope.showAlert(msg, 'success');
-        };
-
-        $scope.showError = function (msg) {
-            $scope.showAlert(msg, 'danger');
-        };
-
-        $scope.showAlert = function (msg, type) {
-            if ($scope.alert)
-                $scope.alert.hide();
-
-            $scope.alert = $alert({
-                type: type,
-                title: msg,
-                duration: 2
-            });
-        };
-
         function reload() {
             $http.post('admin/list')
                 .success(function (data) {
                     $scope.userList = data;
                 })
                 .error(function (errMsg) {
-                    $scope.showError(commonFunctions.errorMessage(errMsg));
+                    commonFunctions.showError(commonFunctions.errorMessage(errMsg));
                 });
         }
 
@@ -57,9 +38,9 @@ controlCenterModule.controller('adminController', ['$scope', '$alert', '$http',
                 function (data) {
                     reload();
 
-                    $scope.showInfo("User has been removed: " + user.username);
-                }).error(function (err) {
-                    $scope.showError("Failed to remove user: " + commonFunctions.errorMessage(errMsg));
+                    commonFunctions.showInfo("User has been removed: " + user.username);
+                }).error(function (errMsg) {
+                    commonFunctions.showError("Failed to remove user: " + commonFunctions.errorMessage(errMsg));
                 });
 
             return false;
@@ -77,7 +58,7 @@ controlCenterModule.controller('adminController', ['$scope', '$alert', '$http',
 
                     adminChanging = false;
                 }).error(function (errMsg) {
-                    $scope.showError("Failed to remove user: " + commonFunctions.errorMessage(errMsg));
+                    commonFunctions.showError("Failed to remove user: " + commonFunctions.errorMessage(errMsg));
 
                     adminChanging = false;
                 });

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/controllers/cache-viewer-controller.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/controllers/cache-viewer-controller.js b/modules/web-control-center/nodejs/controllers/cache-viewer-controller.js
index f294bf9..699d746 100644
--- a/modules/web-control-center/nodejs/controllers/cache-viewer-controller.js
+++ b/modules/web-control-center/nodejs/controllers/cache-viewer-controller.js
@@ -52,8 +52,7 @@ var demoResults = [
 
 var demoCaches = ['Users', 'Organizations', 'Cities'];
 
-controlCenterModule.controller('cacheViewerController', ['$scope', '$alert', '$http', 'commonFunctions', function ($scope, $alert, $http, commonFunctions) {
-
+controlCenterModule.controller('cacheViewerController', ['$scope', '$http', 'commonFunctions', function ($scope, $http, commonFunctions) {
     $scope.results = demoResults;
 
     $scope.caches = demoCaches;

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/controllers/caches-controller.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/controllers/caches-controller.js b/modules/web-control-center/nodejs/controllers/caches-controller.js
index 1853866..4a840d8 100644
--- a/modules/web-control-center/nodejs/controllers/caches-controller.js
+++ b/modules/web-control-center/nodejs/controllers/caches-controller.js
@@ -15,11 +15,10 @@
  * limitations under the License.
  */
 
-controlCenterModule.controller('cachesController', ['$scope', '$alert', '$http', 'commonFunctions', function ($scope, $alert, $http, commonFunctions) {
+controlCenterModule.controller('cachesController', ['$scope', '$http', 'commonFunctions', function ($scope, $http, commonFunctions) {
         $scope.swapSimpleItems = commonFunctions.swapSimpleItems;
         $scope.joinTip = commonFunctions.joinTip;
         $scope.getModel = commonFunctions.getModel;
-        $scope.errorMessage = commonFunctions.errorMessage;
 
         $scope.atomicities = [
             {value: 'ATOMIC', label: 'ATOMIC'},
@@ -76,31 +75,13 @@ controlCenterModule.controller('cachesController', ['$scope', '$alert', '$http',
         $scope.general = [];
         $scope.advanced = [];
 
-        $scope.showError = function (msg) {
-            if ($scope.alert)
-                $scope.alert.hide();
-
-            $scope.alert = $alert({title: $scope.errorMessage(msg)});
-        };
-
-        $scope.showInfo = function (msg) {
-            if ($scope.alert)
-                $scope.alert.hide();
-
-            $scope.alert = $alert({
-                type: 'success',
-                title: msg,
-                duration: 2
-            });
-        };
-
         $http.get('/models/caches.json')
             .success(function (data) {
                 $scope.general = data.general;
                 $scope.advanced = data.advanced;
             })
             .error(function (errMsg) {
-                $scope.showError(errMsg);
+                commonFunctions.showError(errMsg);
             });
 
         $scope.caches = [];
@@ -135,7 +116,7 @@ controlCenterModule.controller('cachesController', ['$scope', '$alert', '$http',
                 }, true);
             })
             .error(function (errMsg) {
-                $scope.showError(errMsg);
+                commonFunctions.showError(errMsg);
             });
 
         $scope.selectItem = function (item) {
@@ -155,13 +136,13 @@ controlCenterModule.controller('cachesController', ['$scope', '$alert', '$http',
             var item = $scope.backupItem;
 
             if (item.cacheStoreFactory && item.cacheStoreFactory.kind && !(item.readThrough || item.writeThrough)) {
-                $scope.showError('Store is configured but read/write through are not enabled!');
+                commonFunctions.showError('Store is configured but read/write through are not enabled!');
 
                 return;
             }
 
             if ((item.readThrough || item.writeThrough) && (!item.cacheStoreFactory || !item.cacheStoreFactory.kind)) {
-                $scope.showError('Read / write through are enabled but strore is not configured!');
+                commonFunctions.showError('Read / write through are enabled but strore is not configured!');
 
                 return;
             }
@@ -182,10 +163,10 @@ controlCenterModule.controller('cachesController', ['$scope', '$alert', '$http',
 
                     $scope.selectItem(item);
 
-                    $scope.showInfo('Cache "' + item.name + '" saved.');
+                    commonFunctions.showInfo('Cache "' + item.name + '" saved.');
                 })
                 .error(function (errMsg) {
-                    $scope.showError(errMsg);
+                    commonFunctions.showError(errMsg);
                 });
         };
 
@@ -206,50 +187,8 @@ controlCenterModule.controller('cachesController', ['$scope', '$alert', '$http',
                     }
                 })
                 .error(function (errMsg) {
-                    $scope.showError(errMsg);
+                    commonFunctions.showError(errMsg);
                 });
         };
-
-        $scope.checkIndexedTypes = function (keyCls, valCls) {
-            if (!keyCls) {
-                $scope.showError('Key class name should be non empty!');
-
-                return false;
-            }
-
-            if (!valCls) {
-                $scope.showError('Value class name should be non empty!');
-
-                return false;
-            }
-
-            return true;
-        };
-
-        $scope.addIndexedTypes = function (keyCls, valCls) {
-            if (!$scope.checkIndexedTypes(keyCls, valCls))
-                return;
-
-            var idxTypes = $scope.backupItem.indexedTypes;
-
-            var newItem = {keyClass: keyCls, valueClass: valCls};
-
-            if (idxTypes)
-                idxTypes.push(newItem);
-            else
-                $scope.backupItem.indexedTypes = [newItem];
-        };
-
-        $scope.saveIndexedType = function (idx, keyCls, valCls) {
-            if (!$scope.checkIndexedTypes(keyCls, valCls))
-                return idx;
-
-            var idxType = $scope.backupItem.indexedTypes[idx];
-
-            idxType.keyClass = keyCls;
-            idxType.valueClass = valCls;
-
-            return -1;
-        };
     }]
 );
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/controllers/clusters-controller.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/controllers/clusters-controller.js b/modules/web-control-center/nodejs/controllers/clusters-controller.js
index cc1369b..10e0135 100644
--- a/modules/web-control-center/nodejs/controllers/clusters-controller.js
+++ b/modules/web-control-center/nodejs/controllers/clusters-controller.js
@@ -15,12 +15,10 @@
  * limitations under the License.
  */
 
-controlCenterModule.controller('clustersController', ['$scope', '$alert', '$http', 'commonFunctions', function ($scope, $alert, $http, commonFunctions) {
+controlCenterModule.controller('clustersController', ['$scope', '$http', 'commonFunctions', function ($scope, $http, commonFunctions) {
         $scope.swapSimpleItems = commonFunctions.swapSimpleItems;
         $scope.joinTip = commonFunctions.joinTip;
         $scope.getModel = commonFunctions.getModel;
-        $scope.errorMessage = commonFunctions.errorMessage;
-        $scope.console = console;
 
         $scope.templates = [
             {value: {discovery: {Vm: {addresses: ['127.0.0.1:47500..47510']}}}, label: 'none'},
@@ -96,7 +94,7 @@ controlCenterModule.controller('clustersController', ['$scope', '$alert', '$http
                 $scope.advanced = data.advanced;
             })
             .error(function (errMsg) {
-                $alert({title: $scope.errorMessage(errMsg)});
+                commonFunctions.showError(errMsg);
             });
 
         // When landing on the page, get clusters and show them.
@@ -130,7 +128,7 @@ controlCenterModule.controller('clustersController', ['$scope', '$alert', '$http
                 }, true);
             })
             .error(function (errMsg) {
-                $alert({title: $scope.errorMessage(errMsg)});
+                commonFunctions.showError(errMsg);
             });
 
         $scope.selectItem = function (item) {
@@ -160,7 +158,7 @@ controlCenterModule.controller('clustersController', ['$scope', '$alert', '$http
                         var cache = $scope.caches[idx];
 
                         if (cache.swapEnabled) {
-                            $alert({title: 'Swap space SPI is not configured, but cache "' + cache.label + '" configured to use swap!'});
+                            commonFunctions.showError('Swap space SPI is not configured, but cache "' + cache.label + '" configured to use swap!');
 
                             return;
                         }
@@ -184,15 +182,10 @@ controlCenterModule.controller('clustersController', ['$scope', '$alert', '$http
 
                     $scope.selectItem(item);
 
-                    $alert({
-                        type: 'success',
-                        title: 'Cluster "' + item.name + '" saved.',
-                        duration: 2,
-                        container: '#save-btn'
-                    });
+                    commonFunctions.showInfo('Cluster "' + item.name + '" saved.');
                 })
                 .error(function (errMsg) {
-                    $alert({title: $scope.errorMessage(errMsg)});
+                    commonFunctions.showError(errMsg);
                 });
         };
 
@@ -213,7 +206,7 @@ controlCenterModule.controller('clustersController', ['$scope', '$alert', '$http
                     }
                 })
                 .error(function (errMsg) {
-                    $alert({title: $scope.errorMessage(errMsg)});
+                    commonFunctions.showError(errMsg);
                 });
         };
     }]

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/controllers/common-module.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/controllers/common-module.js b/modules/web-control-center/nodejs/controllers/common-module.js
index a76d638..fa230db 100644
--- a/modules/web-control-center/nodejs/controllers/common-module.js
+++ b/modules/web-control-center/nodejs/controllers/common-module.js
@@ -17,7 +17,14 @@
 
 var controlCenterModule = angular.module('ignite-web-control-center', ['smart-table', 'mgcrea.ngStrap', 'ngSanitize']);
 
-controlCenterModule.service('commonFunctions', function () {
+// Common functions to be used in controllers.
+controlCenterModule.service('commonFunctions', ['$alert', function ($alert) {
+    var msgModal = undefined;
+
+    function errorMessage(errMsg) {
+        return errMsg ? errMsg : 'Internal server error.';
+    }
+
     return {
         getModel: function (obj, path) {
             if (!path)
@@ -63,11 +70,25 @@ controlCenterModule.service('commonFunctions', function () {
 
             return lines.join("");
         },
-        errorMessage: function (errMsg) {
-            return errMsg ? errMsg : 'Internal server error.';
+        errorMessage: errorMessage,
+        showError: function (msg) {
+            if (msgModal)
+                msgModal.hide();
+
+            msgModal = $alert({title: errorMessage(msg)});
+        },
+        showInfo: function (msg) {
+            if (msgModal)
+                msgModal.hide();
+
+            msgModal = $alert({
+                type: 'success',
+                title: msg,
+                duration: 2
+            });
         }
     }
-});
+}]);
 
 controlCenterModule.config(function ($tooltipProvider) {
     angular.extend($tooltipProvider.defaults, {

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/controllers/metadata-controller.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/controllers/metadata-controller.js b/modules/web-control-center/nodejs/controllers/metadata-controller.js
new file mode 100644
index 0000000..0a0e18c
--- /dev/null
+++ b/modules/web-control-center/nodejs/controllers/metadata-controller.js
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+controlCenterModule.controller('metadataController', ['$scope', '$http', 'commonFunctions', function ($scope, $http, commonFunctions) {
+        $scope.swapSimpleItems = commonFunctions.swapSimpleItems;
+        $scope.joinTip = commonFunctions.joinTip;
+        $scope.getModel = commonFunctions.getModel;
+
+        $scope.metadata = [];
+
+        $http.get('/models/metadata.json')
+            .success(function (data) {
+                $scope.general = data.general;
+            })
+            .error(function (errMsg) {
+                commonFunctions.showError(errMsg);
+            });
+
+        $scope.metadatas = [];
+
+        // When landing on the page, get metadatas and show them.
+        $http.post('metadata/list')
+            .success(function (data) {
+                $scope.spaces = data.spaces;
+                $scope.metadatas = data.metadatas;
+
+                var restoredItem = angular.fromJson(sessionStorage.metadataBackupItem);
+
+                if (restoredItem && restoredItem._id) {
+                    var idx = _.findIndex($scope.metadatas, function (metadata) {
+                        return metadata._id == restoredItem._id;
+                    });
+
+                    if (idx >= 0) {
+                        $scope.selectedItem = $scope.metadatas[idx];
+
+                        $scope.backupItem = restoredItem;
+                    }
+                    else
+                        sessionStorage.removeItem('metadataBackupItem');
+                }
+                else
+                    $scope.backupItem = restoredItem;
+
+                $scope.$watch('backupItem', function (val) {
+                    if (val)
+                        sessionStorage.metadataBackupItem = angular.toJson(val);
+                }, true);
+            })
+            .error(function (errMsg) {
+                commonFunctions.showError(errMsg);
+            });
+
+        $scope.selectItem = function (item) {
+            $scope.selectedItem = item;
+
+            $scope.backupItem = angular.copy(item);
+        };
+
+        // Add new metadata.
+        $scope.createItem = function () {
+            $scope.backupItem = {mode: 'PARTITIONED', atomicityMode: 'ATOMIC', readFromBackup: true};
+            $scope.backupItem.space = $scope.spaces[0]._id;
+        };
+
+        // Save metadata in db.
+        $scope.saveItem = function () {
+            var item = $scope.backupItem;
+
+            $http.post('metadata/save', item)
+                .success(function (_id) {
+                    var idx = _.findIndex($scope.metadatas, function (metadata) {
+                        return metadata._id == _id;
+                    });
+
+                    if (idx >= 0)
+                        angular.extend($scope.metadatas[idx], item);
+                    else {
+                        item._id = _id;
+
+                        $scope.metadatas.push(item);
+                    }
+
+                    $scope.selectItem(item);
+
+                    commonFunctions.showInfo('metadata "' + item.name + '" saved.');
+                })
+                .error(function (errMsg) {
+                    commonFunctions.showError(errMsg);
+                });
+        };
+
+        $scope.removeItem = function () {
+            var _id = $scope.selectedItem._id;
+
+            $http.post('metadata/remove', {_id: _id})
+                .success(function () {
+                    var i = _.findIndex($scope.metadatas, function (metadata) {
+                        return metadata._id == _id;
+                    });
+
+                    if (i >= 0) {
+                        $scope.metadatas.splice(i, 1);
+
+                        $scope.selectedItem = undefined;
+                        $scope.backupItem = undefined;
+                    }
+                })
+                .error(function (errMsg) {
+                    commonFunctions.showError(errMsg);
+                });
+        };
+
+        $scope.checkIndexedTypes = function (keyCls, valCls) {
+            if (!keyCls) {
+                commonFunctions.showError('Key class name should be non empty!');
+
+                return false;
+            }
+
+            if (!valCls) {
+                commonFunctions.showError('Value class name should be non empty!');
+
+                return false;
+            }
+
+            return true;
+        };
+
+        $scope.addIndexedTypes = function (keyCls, valCls) {
+            if (!$scope.checkIndexedTypes(keyCls, valCls))
+                return;
+
+            var idxTypes = $scope.backupItem.indexedTypes;
+
+            var newItem = {keyClass: keyCls, valueClass: valCls};
+
+            if (idxTypes)
+                idxTypes.push(newItem);
+            else
+                $scope.backupItem.indexedTypes = [newItem];
+        };
+
+        $scope.saveIndexedType = function (idx, keyCls, valCls) {
+            if (!$scope.checkIndexedTypes(keyCls, valCls))
+                return idx;
+
+            var idxType = $scope.backupItem.indexedTypes[idx];
+
+            idxType.keyClass = keyCls;
+            idxType.valueClass = valCls;
+
+            return -1;
+        };
+    }]
+);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/controllers/models/metadata.json
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/controllers/models/metadata.json b/modules/web-control-center/nodejs/controllers/models/metadata.json
new file mode 100644
index 0000000..5a125d5
--- /dev/null
+++ b/modules/web-control-center/nodejs/controllers/models/metadata.json
@@ -0,0 +1,11 @@
+{
+  "general": [
+    {
+      "label": "Name",
+      "type": "text",
+      "model": "name",
+      "required": true,
+      "placeholder": "Input name"
+    }
+  ]
+}

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/controllers/persistence-controller.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/controllers/persistence-controller.js b/modules/web-control-center/nodejs/controllers/persistence-controller.js
new file mode 100644
index 0000000..a77c445
--- /dev/null
+++ b/modules/web-control-center/nodejs/controllers/persistence-controller.js
@@ -0,0 +1,213 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+controlCenterModule.controller('persistenceController', ['$scope', '$http', 'commonFunctions', function ($scope, $http, commonFunctions) {
+        $scope.joinTip = commonFunctions.joinTip;
+        $scope.getModel = commonFunctions.getModel;
+
+        $scope.databases = [
+            {value: 'oracle', label: 'Oracle database'},
+            {value: 'db2', label: 'IBM DB2'},
+            {value: 'mssql', label: 'MS SQL Server'},
+            {value: 'postgre', label: 'PostgreSQL'},
+            {value: 'mysql', label: 'MySQL'},
+            {value: 'h2', label: 'H2 database'}
+        ];
+
+        $scope.connection = [];
+
+        $http.get('/models/persistence.json')
+            .success(function (data) {
+                $scope.connection = data.connection;
+            })
+            .error(function (errMsg) {
+                commonFunctions.showError(errMsg);
+            });
+
+        $scope.persistences = [];
+
+        // When landing on the page, get persistences and show them.
+        $http.post('persistence/list')
+            .success(function (data) {
+                $scope.spaces = data.spaces;
+                $scope.persistences = data.persistences;
+
+                var restoredItem = angular.fromJson(sessionStorage.persistenceBackupItem);
+
+                if (restoredItem && restoredItem._id) {
+                    var idx = _.findIndex($scope.persistences, function (persistence) {
+                        return persistence._id == restoredItem._id;
+                    });
+
+                    if (idx >= 0) {
+                        $scope.selectedItem = $scope.persistences[idx];
+
+                        $scope.backupItem = restoredItem;
+                    }
+                    else
+                        sessionStorage.removeItem('persistenceBackupItem');
+                }
+                else
+                    $scope.backupItem = restoredItem;
+
+                $scope.$watch('backupItem', function (val) {
+                    if (val)
+                        sessionStorage.persistenceBackupItem = angular.toJson(val);
+                }, true);
+            })
+            .error(function (errMsg) {
+                commonFunctions.showError(errMsg);
+            });
+
+        $scope.selectItem = function (item) {
+            $scope.selectedItem = item;
+            $scope.backupItem = angular.copy(item);
+        };
+
+        // Add new persistence.
+        $scope.createItem = function () {
+            $scope.backupItem = {database: 'oracle'};
+            $scope.backupItem.space = $scope.spaces[0]._id;
+        };
+
+        // Save persistence in db.
+        $scope.saveItem = function () {
+            var item = $scope.backupItem;
+
+            $http.post('save', item)
+                .success(function (_id) {
+                    var i = _.findIndex($scope.persistences, function (persistence) {
+                        return persistence._id == _id;
+                    });
+
+                    if (i >= 0)
+                        angular.extend($scope.persistences[i], item);
+                    else {
+                        item._id = _id;
+
+                        $scope.persistences.push(item);
+                    }
+
+                    $scope.selectItem(item);
+                })
+                .error(function (errMsg) {
+                    commonFunctions.showError(errMsg);
+                });
+        };
+
+        $scope.removeItem = function () {
+            var _id = $scope.selectedItem._id;
+
+            $http.post('remove', {_id: _id})
+                .success(function () {
+                    var i = _.findIndex($scope.persistences, function (persistence) {
+                        return persistence._id == _id;
+                    });
+
+                    if (i >= 0) {
+                        $scope.persistences.splice(i, 1);
+
+                        $scope.selectedItem = undefined;
+                        $scope.backupItem = undefined;
+                    }
+                })
+                .error(function (errMsg) {
+                    commonFunctions.showError(errMsg);
+                });
+        };
+
+        $scope.data = {
+            curTableIdx: -1,
+            curFieldIdx: -1,
+            curKeyClass: '',
+            curValueClass: '',
+            curJavaName: '',
+            curJavaType: '',
+            tables: [
+                {schemaName: 'Schema1', use: true},
+                {schemaName: 'Schema1', use: true, tableName: 'Table1', keyClass: 'KeyClass1', valueClass: 'ValueClass1',
+                    fields: [
+                        {use: true, key: true, ak: true, dbName: 'name1', dbType: 'dbType1', javaName: 'javaName1', javaType: 'javaType1'},
+                        {use: true, key: false, ak: false, dbName: 'name2', dbType: 'dbType2', javaName: 'javaName2', javaType: 'javaType2'},
+                        {use: false, key: false, ak: false, dbName: 'name3', dbType: 'dbType3', javaName: 'javaName3', javaType: 'javaType3'}
+                    ]
+                },
+                {schemaName: 'Schema2 with very long name', use: false},
+                {schemaName: 'Schema2', use: false, tableName: 'Table2', keyClass: 'KeyClass2', valueClass: 'ValueClass2',
+                    fields: [
+                        {use: true, key: true, ak: true, dbName: 'name4', dbType: 'dbType4', javaName: 'javaName4', javaType: 'javaType4'},
+                        {use: true, key: false, ak: false, dbName: 'name5', dbType: 'dbType5', javaName: 'javaName5', javaType: 'javaType5'},
+                        {use: false, key: false, ak: false, dbName: 'name6', dbType: 'dbType6', javaName: 'javaName6', javaType: 'javaType6'}
+                    ]},
+                {schemaName: 'Schema2', use: false, tableName: 'Table3', keyClass: 'KeyClass3', valueClass: 'ValueClass3',
+                    fields: [
+                        {use: true, key: true, ak: true, dbName: 'name7', dbType: 'dbType7', javaName: 'javaName7', javaType: 'javaType7'},
+                        {use: true, key: false, ak: false, dbName: 'name8', dbType: 'dbType8', javaName: 'javaName8', javaType: 'javaType8'},
+                        {use: false, key: false, ak: false, dbName: 'name9', dbType: 'dbType9', javaName: 'javaName9', javaType: 'javaType9'},
+                        {use: false, key: false, ak: false, dbName: 'name10', dbType: 'dbType10', javaName: 'javaName10', javaType: 'javaType10'},
+                        {use: false, key: false, ak: false, dbName: 'name11', dbType: 'dbType11', javaName: 'javaName11', javaType: 'javaType11'},
+                        {use: false, key: false, ak: false, dbName: 'name12', dbType: 'dbType12', javaName: 'javaName12', javaType: 'javaType12'}
+                    ]}]
+        };
+
+        $scope.selectSchema = function (idx) {
+            var data = $scope.data;
+            var tables = data.tables;
+            var schemaName = tables[idx].schemaName;
+            var use = tables[idx].use;
+
+            for (var i = idx + 1; i < tables.length; i++) {
+                var item = tables[i];
+
+                if (item.schemaName == schemaName && item.tableName)
+                    item.use = use;
+                else
+                    break;
+            }
+
+            data.curTableIdx = -1;
+            data.curFieldIdx = -1;
+        };
+
+        $scope.selectTable = function (idx) {
+            var data = $scope.data;
+
+            data.curTableIdx = idx;
+            data.curFieldIdx = -1;
+
+            if (idx >= 0) {
+                var tbl = data.tables[idx];
+
+                data.curKeyClass = tbl.keyClass;
+                data.curValueClass = tbl.valueClass;
+            }
+        };
+
+        $scope.selectField = function (idx) {
+            var data = $scope.data;
+
+            data.curFieldIdx = idx;
+
+            if (idx >= 0) {
+                var fld = data.tables[data.curTableIdx].fields[idx];
+
+                data.curJavaName = fld.javaName;
+                data.curJavaType = fld.javaType;
+            }
+        };
+    }]
+);

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/controllers/persistences-controller.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/controllers/persistences-controller.js b/modules/web-control-center/nodejs/controllers/persistences-controller.js
deleted file mode 100644
index 3d3f8cc..0000000
--- a/modules/web-control-center/nodejs/controllers/persistences-controller.js
+++ /dev/null
@@ -1,214 +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.
- */
-
-controlCenterModule.controller('persistenceController', ['$scope', '$alert', '$http', 'commonFunctions', function ($scope, $alert, $http, commonFunctions) {
-        $scope.joinTip = commonFunctions.joinTip;
-        $scope.getModel = commonFunctions.getModel;
-        $scope.errorMessage = commonFunctions.errorMessage;
-
-        $scope.databases = [
-            {value: 'oracle', label: 'Oracle database'},
-            {value: 'db2', label: 'IBM DB2'},
-            {value: 'mssql', label: 'MS SQL Server'},
-            {value: 'postgre', label: 'PostgreSQL'},
-            {value: 'mysql', label: 'MySQL'},
-            {value: 'h2', label: 'H2 database'}
-        ];
-
-        $scope.connection = [];
-
-        $http.get('/models/persistence.json')
-            .success(function (data) {
-                $scope.connection = data.connection;
-            })
-            .error(function (errMsg) {
-                $alert({title: $scope.errorMessage(errMsg)});
-            });
-
-        $scope.persistences = [];
-
-        // When landing on the page, get persistences and show them.
-        $http.post('list')
-            .success(function (data) {
-                $scope.spaces = data.spaces;
-                $scope.persistences = data.persistences;
-
-                var restoredItem = angular.fromJson(sessionStorage.persistenceBackupItem);
-
-                if (restoredItem && restoredItem._id) {
-                    var idx = _.findIndex($scope.persistences, function (persistence) {
-                        return persistence._id == restoredItem._id;
-                    });
-
-                    if (idx >= 0) {
-                        $scope.selectedItem = $scope.persistences[idx];
-
-                        $scope.backupItem = restoredItem;
-                    }
-                    else
-                        sessionStorage.removeItem('persistenceBackupItem');
-                }
-                else
-                    $scope.backupItem = restoredItem;
-
-                $scope.$watch('backupItem', function (val) {
-                    if (val)
-                        sessionStorage.persistenceBackupItem = angular.toJson(val);
-                }, true);
-            })
-            .error(function (errMsg) {
-                $alert({title: $scope.errorMessage(errMsg)});
-            });
-
-        $scope.selectItem = function (item) {
-            $scope.selectedItem = item;
-            $scope.backupItem = angular.copy(item);
-        };
-
-        // Add new persistence.
-        $scope.createItem = function () {
-            $scope.backupItem = {database: 'oracle'};
-            $scope.backupItem.space = $scope.spaces[0]._id;
-        };
-
-        // Save persistence in db.
-        $scope.saveItem = function () {
-            var item = $scope.backupItem;
-
-            $http.post('save', item)
-                .success(function (_id) {
-                    var i = _.findIndex($scope.persistences, function (persistence) {
-                        return persistence._id == _id;
-                    });
-
-                    if (i >= 0)
-                        angular.extend($scope.persistences[i], item);
-                    else {
-                        item._id = _id;
-
-                        $scope.persistences.push(item);
-                    }
-
-                    $scope.selectItem(item);
-                })
-                .error(function (errMsg) {
-                    $alert({title: $scope.errorMessage(errMsg)});
-                });
-        };
-
-        $scope.removeItem = function () {
-            var _id = $scope.selectedItem._id;
-
-            $http.post('remove', {_id: _id})
-                .success(function () {
-                    var i = _.findIndex($scope.persistences, function (persistence) {
-                        return persistence._id == _id;
-                    });
-
-                    if (i >= 0) {
-                        $scope.persistences.splice(i, 1);
-
-                        $scope.selectedItem = undefined;
-                        $scope.backupItem = undefined;
-                    }
-                })
-                .error(function (errMsg) {
-                    $alert({title: $scope.errorMessage(errMsg)});
-                });
-        };
-
-        $scope.data = {
-            curTableIdx: -1,
-            curFieldIdx: -1,
-            curKeyClass: '',
-            curValueClass: '',
-            curJavaName: '',
-            curJavaType: '',
-            tables: [
-                {schemaName: 'Schema1', use: true},
-                {schemaName: 'Schema1', use: true, tableName: 'Table1', keyClass: 'KeyClass1', valueClass: 'ValueClass1',
-                    fields: [
-                        {use: true, key: true, ak: true, dbName: 'name1', dbType: 'dbType1', javaName: 'javaName1', javaType: 'javaType1'},
-                        {use: true, key: false, ak: false, dbName: 'name2', dbType: 'dbType2', javaName: 'javaName2', javaType: 'javaType2'},
-                        {use: false, key: false, ak: false, dbName: 'name3', dbType: 'dbType3', javaName: 'javaName3', javaType: 'javaType3'}
-                    ]
-                },
-                {schemaName: 'Schema2 with very long name', use: false},
-                {schemaName: 'Schema2', use: false, tableName: 'Table2', keyClass: 'KeyClass2', valueClass: 'ValueClass2',
-                    fields: [
-                        {use: true, key: true, ak: true, dbName: 'name4', dbType: 'dbType4', javaName: 'javaName4', javaType: 'javaType4'},
-                        {use: true, key: false, ak: false, dbName: 'name5', dbType: 'dbType5', javaName: 'javaName5', javaType: 'javaType5'},
-                        {use: false, key: false, ak: false, dbName: 'name6', dbType: 'dbType6', javaName: 'javaName6', javaType: 'javaType6'}
-                    ]},
-                {schemaName: 'Schema2', use: false, tableName: 'Table3', keyClass: 'KeyClass3', valueClass: 'ValueClass3',
-                    fields: [
-                        {use: true, key: true, ak: true, dbName: 'name7', dbType: 'dbType7', javaName: 'javaName7', javaType: 'javaType7'},
-                        {use: true, key: false, ak: false, dbName: 'name8', dbType: 'dbType8', javaName: 'javaName8', javaType: 'javaType8'},
-                        {use: false, key: false, ak: false, dbName: 'name9', dbType: 'dbType9', javaName: 'javaName9', javaType: 'javaType9'},
-                        {use: false, key: false, ak: false, dbName: 'name10', dbType: 'dbType10', javaName: 'javaName10', javaType: 'javaType10'},
-                        {use: false, key: false, ak: false, dbName: 'name11', dbType: 'dbType11', javaName: 'javaName11', javaType: 'javaType11'},
-                        {use: false, key: false, ak: false, dbName: 'name12', dbType: 'dbType12', javaName: 'javaName12', javaType: 'javaType12'}
-                    ]}]
-        };
-
-        $scope.selectSchema = function (idx) {
-            var data = $scope.data;
-            var tables = data.tables;
-            var schemaName = tables[idx].schemaName;
-            var use = tables[idx].use;
-
-            for (var i = idx + 1; i < tables.length; i++) {
-                var item = tables[i];
-
-                if (item.schemaName == schemaName && item.tableName)
-                    item.use = use;
-                else
-                    break;
-            }
-
-            data.curTableIdx = -1;
-            data.curFieldIdx = -1;
-        };
-
-        $scope.selectTable = function (idx) {
-            var data = $scope.data;
-
-            data.curTableIdx = idx;
-            data.curFieldIdx = -1;
-
-            if (idx >= 0) {
-                var tbl = data.tables[idx];
-
-                data.curKeyClass = tbl.keyClass;
-                data.curValueClass = tbl.valueClass;
-            }
-        };
-
-        $scope.selectField = function (idx) {
-            var data = $scope.data;
-
-            data.curFieldIdx = idx;
-
-            if (idx >= 0) {
-                var fld = data.tables[data.curTableIdx].fields[idx];
-
-                data.curJavaName = fld.javaName;
-                data.curJavaType = fld.javaType;
-            }
-        };
-    }]
-);

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/controllers/profile-controller.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/controllers/profile-controller.js b/modules/web-control-center/nodejs/controllers/profile-controller.js
index 9c27bc2..6579d8d 100644
--- a/modules/web-control-center/nodejs/controllers/profile-controller.js
+++ b/modules/web-control-center/nodejs/controllers/profile-controller.js
@@ -15,30 +15,9 @@
  * limitations under the License.
  */
 
-controlCenterModule.controller('profileController', ['$scope', '$alert', '$http', 'commonFunctions',
-    function ($scope, $alert, $http, commonFunctions) {
-    
+controlCenterModule.controller('profileController', ['$scope', '$http', 'commonFunctions', function ($scope, $http, commonFunctions) {
     $scope.profileUser = angular.copy($scope.loggedInUser);
 
-    $scope.showInfo = function (msg) {
-        $scope.showAlert(msg, 'success');
-    };
-
-    $scope.showError = function (msg) {
-        $scope.showAlert(msg, 'danger');
-    };
-
-    $scope.showAlert = function (msg, type) {
-        if ($scope.alert)
-            $scope.alert.hide();
-
-        $scope.alert = $alert({
-            type: type,
-            title: msg,
-            duration: 2
-        });
-    };
-
     $scope.saveUser = function() {
         var profile = $scope.profileUser;
 
@@ -56,7 +35,7 @@ controlCenterModule.controller('profileController', ['$scope', '$alert', '$http'
                     email: changeEmail ? email : undefined,
                     newPassword: profile.changePassword ? profile.newPassword : undefined
                 }).success(function () {
-                    $scope.showInfo('Profile saved.');
+                    commonFunctions.showInfo('Profile saved.');
 
                     if (changeUsername)
                         $scope.loggedInUser.username = userName;
@@ -64,7 +43,7 @@ controlCenterModule.controller('profileController', ['$scope', '$alert', '$http'
                     if (changeEmail)
                         $scope.loggedInUser.email = email;
                 }).error(function (err) {
-                    $scope.showError('Failed to save profile: ' + commonFunctions.errorMessage(err));
+                    commonFunctions.showError('Failed to save profile: ' + commonFunctions.errorMessage(err));
                 });
             }
         }

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/db.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/db.js b/modules/web-control-center/nodejs/db.js
index 7d0f6ce..101d4ae 100644
--- a/modules/web-control-center/nodejs/db.js
+++ b/modules/web-control-center/nodejs/db.js
@@ -48,6 +48,14 @@ exports.Space = mongoose.model('Space', new Schema({
     }]
 }));
 
+// Define cache type metadata model.
+var CacheTypeMetadataSchema = new Schema({
+    space: {type: ObjectId, ref: 'Space'},
+    name: String
+});
+
+exports.CacheTypeMetadata = mongoose.model('CacheTypeMetadata', CacheTypeMetadataSchema);
+
 // Define cache model.
 var CacheSchema = new Schema({
     space: {type: ObjectId, ref: 'Space'},

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/routes/metadata.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/routes/metadata.js b/modules/web-control-center/nodejs/routes/metadata.js
new file mode 100644
index 0000000..fbafbfa
--- /dev/null
+++ b/modules/web-control-center/nodejs/routes/metadata.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.
+ */
+
+var router = require('express').Router();
+var db = require('../db');
+
+/* GET metadata page. */
+router.get('/', function(req, res) {
+    res.render('configuration/metadata');
+});
+
+/**
+ * Get spaces and metadata accessed for user account.
+ *
+ * @param req Request.
+ * @param res Response.
+ */
+router.post('/list', function(req, res) {
+    var user_id = req.currentUserId();
+
+    // Get owned space and all accessed space.
+    db.Space.find({$or: [{owner: user_id}, {usedBy: {$elemMatch: {account: user_id}}}]}, function (err, spaces) {
+        if (err)
+            return res.status(500).send(err.message);
+
+        var space_ids = spaces.map(function(value) {
+            return value._id;
+        });
+
+        // Get all metadata for spaces.
+        db.CacheTypeMetadata.find({space: {$in: space_ids}}, function (err, metadata) {
+            if (err)
+                return res.status(500).send(err.message);
+
+            res.json({spaces: spaces, metadata: metadata});
+        });
+    });
+});
+
+/**
+ * Save metadata.
+ */
+router.post('/save', function(req, res) {
+    if (req.body._id)
+        db.CacheTypeMetadata.update({_id: req.body._id}, req.body, {upsert: true}, function(err) {
+            if (err)
+                return res.status(500).send(err.message);
+
+            res.send(req.body._id);
+        });
+    else {
+        var metadata = new db.CacheTypeMetadata(req.body);
+
+        metadata.save(function(err, metadata) {
+            if (err)
+                return res.status(500).send(err.message);
+
+            res.send(metadata._id);
+        });
+    }
+});
+
+/**
+ * Remove metadata by ._id.
+ */
+router.post('/remove', function(req, res) {
+    db.CacheTypeMetadata.remove(req.body, function (err) {
+        if (err)
+            return res.status(500).send(err.message);
+
+        res.sendStatus(200);
+    })
+});
+
+module.exports = router;
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/routes/persistence.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/routes/persistence.js b/modules/web-control-center/nodejs/routes/persistence.js
new file mode 100644
index 0000000..5b33f9b
--- /dev/null
+++ b/modules/web-control-center/nodejs/routes/persistence.js
@@ -0,0 +1,311 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var router = require('express').Router();
+var db = require('../db');
+var ds = require('../helpers/data-structures.js'), jdbcTypes = ds.jdbcTypes, javaTypes = ds.javaTypes;
+
+/* GET persistence page. */
+router.get('/', function(req, res) {
+    res.render('configuration/persistence');
+});
+
+/**
+ * Get spaces and persistences accessed for user account.
+ *
+ * @param req Request.
+ * @param res Response.
+ */
+router.post('/list', function(req, res) {
+    var user_id = req.currentUserId();
+
+    // Get owned space and all accessed space.
+    db.Space.find({$or: [{owner: user_id}, {usedBy: {$elemMatch: {account: user_id}}}]}, function (err, spaces) {
+        if (err)
+            return res.status(500).send(err.message);
+
+        var space_ids = spaces.map(function(value) {
+            return value._id;
+        });
+
+        // Get all persistences for spaces.
+        db.Persistence.find({space: {$in: space_ids}}, function (err, persistences) {
+            if (err)
+                return res.status(500).send(err.message);
+
+            res.json({spaces: spaces, persistences: persistences});
+        });
+    });
+});
+
+/**
+ * Save persistence.
+ */
+router.post('/save', function(req, res) {
+    if (req.body._id)
+        db.Persistence.update({_id: req.body._id}, req.body, {upsert: true}, function(err) {
+            if (err)
+                return res.status(500).send(err.message);
+
+            res.send(req.body._id);
+        });
+    else {
+        var persistence = new db.Persistence(req.body);
+
+        persistence.save(function(err, persistence) {
+            if (err)
+                return res.status(500).send(err.message);
+
+            res.send(persistence._id);
+        });
+    }
+});
+
+/**
+ * Remove persistence by ._id.
+ */
+router.post('/remove', function(req, res) {
+    db.Persistence.remove(req.body, function (err) {
+        if (err)
+            return res.status(500).send(err.message);
+
+        res.sendStatus(200);
+    })
+});
+
+// simple countdown latch
+function CDL(countdown, completion) {
+    this.countDown = function() {
+        if(--countdown < 1) completion();
+    };
+}
+
+/**
+ * @param name Source name.
+ * @return String converted to java class name notation.
+ */
+function toJavaClassName(name) {
+    var len = name.length;
+
+    var buf = [];
+
+    var capitalizeNext = true;
+
+    for (var i = 0; i < len; i++) {
+        var ch = name.charAt(i);
+
+        if (' ' == ch || '_' == ch)
+            capitalizeNext = true;
+        else if (capitalizeNext) {
+            buf.push(ch.toUpperCase());
+
+            capitalizeNext = false;
+        }
+        else
+            buf.push(ch.toLowerCase());
+    }
+
+    return buf.join("");
+}
+
+/**
+ * @param name Source name.
+ * @return String converted to java field name notation.
+ */
+function toJavaFieldName(name) {
+    var javaName = toJavaClassName(name);
+
+    return javaName.charAt(0).toLowerCase() + javaName.substring(1);
+}
+
+
+//
+router.post('/pg', function(req, res) {
+    var pg = require('pg');
+    var util = require('util');
+
+    var host = req.body.host;
+    var port = req.body.port;
+
+    var username = req.body.username;
+    var password = req.body.password;
+
+    var dbName = req.body.dbName;
+
+    var connectionString = util.format('postgres://%s:%s@%s:%d/%s', username, password, host, port, dbName);
+
+    pg.connect(connectionString, function(err, client, done) {
+        var sendError = function (err) {
+            done();
+
+            res.status(500).send(err.message);
+        };
+
+        if(err)
+            return sendError(err);
+
+        var sendResponse = function () {
+            done();
+
+            console.log(JSON.stringify(tables));
+
+            res.status(200).send(tables);
+        }, jdbcType = function (dataType) {
+            switch (dataType) {
+                case 'smallint':
+                case 'int2':
+                    return jdbcTypes.SMALLINT;
+                case 'integer':
+                case 'int':
+                case 'int4':
+                    return jdbcTypes.INTEGER;
+                case 'oid':
+                case 'bigint':
+                case 'int8':
+                    return jdbcTypes.BIGINT;
+                case 'money':
+                    return jdbcTypes.DOUBLE;
+                case 'decimal':
+                case 'numeric':
+                    return jdbcTypes.NUMERIC;
+                case 'float4':
+                    return jdbcTypes.REAL;
+                case 'float':
+                case 'float8':
+                    return jdbcTypes.DOUBLE;
+                case 'char':
+                case 'bpchar':
+                    return jdbcTypes.CHAR;
+                case 'varchar':
+                case 'text':
+                case 'name':
+                    return jdbcTypes.VARCHAR;
+                case 'bytea':
+                    return jdbcTypes.BINARY;
+                case 'boolean':
+                case 'bool':
+                case 'bit':
+                    return jdbcTypes.BIT;
+                case 'date':
+                    return jdbcTypes.DATE;
+                case 'time':
+                case 'timetz':
+                    return jdbcTypes.TIME;
+                case 'timestamp':
+                case 'timestamptz':
+                    return jdbcTypes.TIMESTAMP;
+            }
+        }, javaType = function (dataType) {
+            switch (dataType) {
+                case jdbcTypes.SMALLINT:
+                case jdbcTypes.INTEGER:
+                    return javaTypes.INTEGER;
+                case jdbcTypes.BIGINT:
+                    return javaTypes.LONG;
+                case jdbcTypes.DOUBLE:
+                    return javaTypes.DOUBLE;
+                case jdbcTypes.NUMERIC:
+                    return javaTypes.BIGDECIMAL;
+                case jdbcTypes.REAL:
+                    return javaTypes.FLOAT;
+                case jdbcTypes.CHAR:
+                case jdbcTypes.VARCHAR:
+                    return javaTypes.STRING;
+                case jdbcTypes.BINARY:
+                    return javaTypes.BYTE_ARRAY;
+                case jdbcTypes.BIT:
+                    return javaTypes.BOOLEAN;
+                case jdbcTypes.DATE:
+                    return javaTypes.DATE;
+                case jdbcTypes.TIME:
+                    return javaTypes.TIME;
+                case jdbcTypes.TIMESTAMP:
+                    return javaTypes.TIMESTAMP;
+            }
+        };
+
+        var tables = [];
+
+        client.query(
+            'SELECT table_schema, table_name ' +
+            'FROM information_schema.tables ' +
+            'WHERE table_schema = ANY (current_schemas(false)) ' +
+            'ORDER BY table_schema, table_name', function(err, result) {
+
+            if(err)
+                return sendError(err);
+
+            if (result.rows.length > 0) {
+                // usage
+                var latch = new CDL(result.rows.length, sendResponse);
+
+                result.rows.forEach(function (table) {
+
+                    var indisprimary = client.query(
+                        "SELECT a.attname " +
+                        "FROM pg_index i " +
+                        "JOIN pg_attribute a " +
+                        "  ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) " +
+                        "WHERE  i.indrelid = $1::regclass AND i.indisprimary", [table.table_schema + '.' + table.table_name],
+                        function (err, result) {
+                            if (err)
+                                return sendError(err);
+
+                            var pks = result.rows.map(function(row) {
+                                return row.attname;
+                            });
+
+                            client.query(
+                                'SELECT column_name, udt_name, is_nullable ' +
+                                'FROM information_schema.columns ' +
+                                'WHERE table_schema = $1 AND table_name = $2', [table.table_schema, table.table_name],
+                                function (err, result) {
+                                    if (err)
+                                        return sendError(err);
+
+                                    var cols = [];
+
+                                    result.rows.forEach(function (column) {
+                                        var dataType = jdbcType(column.udt_name);
+
+                                        cols.push({
+                                            pk: pks.indexOf(column.column_name) >= 0,
+                                            use: true,
+                                            notNull: column.is_nullable == 'NO',
+                                            dbName: column.column_name, dbType: dataType,
+                                            javaName: toJavaFieldName(column.column_name), javaType: javaType(dataType) });
+                                    });
+
+                                    var valClsName = toJavaClassName(table.table_name);
+
+                                    tables.push({
+                                        use: pks.length > 0,
+                                        schemaName: table.table_schema, tableName: table.table_name,
+                                        keyClass: valClsName + 'Key', valueClass: valClsName,
+                                        columns: cols
+                                    });
+
+                                    latch.countDown();
+                                })
+                        });
+                });
+            }
+        });
+    });
+});
+
+module.exports = router;
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/routes/persistences.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/routes/persistences.js b/modules/web-control-center/nodejs/routes/persistences.js
deleted file mode 100644
index 5b33f9b..0000000
--- a/modules/web-control-center/nodejs/routes/persistences.js
+++ /dev/null
@@ -1,311 +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.
- */
-
-var router = require('express').Router();
-var db = require('../db');
-var ds = require('../helpers/data-structures.js'), jdbcTypes = ds.jdbcTypes, javaTypes = ds.javaTypes;
-
-/* GET persistence page. */
-router.get('/', function(req, res) {
-    res.render('configuration/persistence');
-});
-
-/**
- * Get spaces and persistences accessed for user account.
- *
- * @param req Request.
- * @param res Response.
- */
-router.post('/list', function(req, res) {
-    var user_id = req.currentUserId();
-
-    // Get owned space and all accessed space.
-    db.Space.find({$or: [{owner: user_id}, {usedBy: {$elemMatch: {account: user_id}}}]}, function (err, spaces) {
-        if (err)
-            return res.status(500).send(err.message);
-
-        var space_ids = spaces.map(function(value) {
-            return value._id;
-        });
-
-        // Get all persistences for spaces.
-        db.Persistence.find({space: {$in: space_ids}}, function (err, persistences) {
-            if (err)
-                return res.status(500).send(err.message);
-
-            res.json({spaces: spaces, persistences: persistences});
-        });
-    });
-});
-
-/**
- * Save persistence.
- */
-router.post('/save', function(req, res) {
-    if (req.body._id)
-        db.Persistence.update({_id: req.body._id}, req.body, {upsert: true}, function(err) {
-            if (err)
-                return res.status(500).send(err.message);
-
-            res.send(req.body._id);
-        });
-    else {
-        var persistence = new db.Persistence(req.body);
-
-        persistence.save(function(err, persistence) {
-            if (err)
-                return res.status(500).send(err.message);
-
-            res.send(persistence._id);
-        });
-    }
-});
-
-/**
- * Remove persistence by ._id.
- */
-router.post('/remove', function(req, res) {
-    db.Persistence.remove(req.body, function (err) {
-        if (err)
-            return res.status(500).send(err.message);
-
-        res.sendStatus(200);
-    })
-});
-
-// simple countdown latch
-function CDL(countdown, completion) {
-    this.countDown = function() {
-        if(--countdown < 1) completion();
-    };
-}
-
-/**
- * @param name Source name.
- * @return String converted to java class name notation.
- */
-function toJavaClassName(name) {
-    var len = name.length;
-
-    var buf = [];
-
-    var capitalizeNext = true;
-
-    for (var i = 0; i < len; i++) {
-        var ch = name.charAt(i);
-
-        if (' ' == ch || '_' == ch)
-            capitalizeNext = true;
-        else if (capitalizeNext) {
-            buf.push(ch.toUpperCase());
-
-            capitalizeNext = false;
-        }
-        else
-            buf.push(ch.toLowerCase());
-    }
-
-    return buf.join("");
-}
-
-/**
- * @param name Source name.
- * @return String converted to java field name notation.
- */
-function toJavaFieldName(name) {
-    var javaName = toJavaClassName(name);
-
-    return javaName.charAt(0).toLowerCase() + javaName.substring(1);
-}
-
-
-//
-router.post('/pg', function(req, res) {
-    var pg = require('pg');
-    var util = require('util');
-
-    var host = req.body.host;
-    var port = req.body.port;
-
-    var username = req.body.username;
-    var password = req.body.password;
-
-    var dbName = req.body.dbName;
-
-    var connectionString = util.format('postgres://%s:%s@%s:%d/%s', username, password, host, port, dbName);
-
-    pg.connect(connectionString, function(err, client, done) {
-        var sendError = function (err) {
-            done();
-
-            res.status(500).send(err.message);
-        };
-
-        if(err)
-            return sendError(err);
-
-        var sendResponse = function () {
-            done();
-
-            console.log(JSON.stringify(tables));
-
-            res.status(200).send(tables);
-        }, jdbcType = function (dataType) {
-            switch (dataType) {
-                case 'smallint':
-                case 'int2':
-                    return jdbcTypes.SMALLINT;
-                case 'integer':
-                case 'int':
-                case 'int4':
-                    return jdbcTypes.INTEGER;
-                case 'oid':
-                case 'bigint':
-                case 'int8':
-                    return jdbcTypes.BIGINT;
-                case 'money':
-                    return jdbcTypes.DOUBLE;
-                case 'decimal':
-                case 'numeric':
-                    return jdbcTypes.NUMERIC;
-                case 'float4':
-                    return jdbcTypes.REAL;
-                case 'float':
-                case 'float8':
-                    return jdbcTypes.DOUBLE;
-                case 'char':
-                case 'bpchar':
-                    return jdbcTypes.CHAR;
-                case 'varchar':
-                case 'text':
-                case 'name':
-                    return jdbcTypes.VARCHAR;
-                case 'bytea':
-                    return jdbcTypes.BINARY;
-                case 'boolean':
-                case 'bool':
-                case 'bit':
-                    return jdbcTypes.BIT;
-                case 'date':
-                    return jdbcTypes.DATE;
-                case 'time':
-                case 'timetz':
-                    return jdbcTypes.TIME;
-                case 'timestamp':
-                case 'timestamptz':
-                    return jdbcTypes.TIMESTAMP;
-            }
-        }, javaType = function (dataType) {
-            switch (dataType) {
-                case jdbcTypes.SMALLINT:
-                case jdbcTypes.INTEGER:
-                    return javaTypes.INTEGER;
-                case jdbcTypes.BIGINT:
-                    return javaTypes.LONG;
-                case jdbcTypes.DOUBLE:
-                    return javaTypes.DOUBLE;
-                case jdbcTypes.NUMERIC:
-                    return javaTypes.BIGDECIMAL;
-                case jdbcTypes.REAL:
-                    return javaTypes.FLOAT;
-                case jdbcTypes.CHAR:
-                case jdbcTypes.VARCHAR:
-                    return javaTypes.STRING;
-                case jdbcTypes.BINARY:
-                    return javaTypes.BYTE_ARRAY;
-                case jdbcTypes.BIT:
-                    return javaTypes.BOOLEAN;
-                case jdbcTypes.DATE:
-                    return javaTypes.DATE;
-                case jdbcTypes.TIME:
-                    return javaTypes.TIME;
-                case jdbcTypes.TIMESTAMP:
-                    return javaTypes.TIMESTAMP;
-            }
-        };
-
-        var tables = [];
-
-        client.query(
-            'SELECT table_schema, table_name ' +
-            'FROM information_schema.tables ' +
-            'WHERE table_schema = ANY (current_schemas(false)) ' +
-            'ORDER BY table_schema, table_name', function(err, result) {
-
-            if(err)
-                return sendError(err);
-
-            if (result.rows.length > 0) {
-                // usage
-                var latch = new CDL(result.rows.length, sendResponse);
-
-                result.rows.forEach(function (table) {
-
-                    var indisprimary = client.query(
-                        "SELECT a.attname " +
-                        "FROM pg_index i " +
-                        "JOIN pg_attribute a " +
-                        "  ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) " +
-                        "WHERE  i.indrelid = $1::regclass AND i.indisprimary", [table.table_schema + '.' + table.table_name],
-                        function (err, result) {
-                            if (err)
-                                return sendError(err);
-
-                            var pks = result.rows.map(function(row) {
-                                return row.attname;
-                            });
-
-                            client.query(
-                                'SELECT column_name, udt_name, is_nullable ' +
-                                'FROM information_schema.columns ' +
-                                'WHERE table_schema = $1 AND table_name = $2', [table.table_schema, table.table_name],
-                                function (err, result) {
-                                    if (err)
-                                        return sendError(err);
-
-                                    var cols = [];
-
-                                    result.rows.forEach(function (column) {
-                                        var dataType = jdbcType(column.udt_name);
-
-                                        cols.push({
-                                            pk: pks.indexOf(column.column_name) >= 0,
-                                            use: true,
-                                            notNull: column.is_nullable == 'NO',
-                                            dbName: column.column_name, dbType: dataType,
-                                            javaName: toJavaFieldName(column.column_name), javaType: javaType(dataType) });
-                                    });
-
-                                    var valClsName = toJavaClassName(table.table_name);
-
-                                    tables.push({
-                                        use: pks.length > 0,
-                                        schemaName: table.table_schema, tableName: table.table_name,
-                                        keyClass: valClsName + 'Key', valueClass: valClsName,
-                                        columns: cols
-                                    });
-
-                                    latch.countDown();
-                                })
-                        });
-                });
-            }
-        });
-    });
-});
-
-module.exports = router;
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/tests/routes/persistence.js
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/tests/routes/persistence.js b/modules/web-control-center/nodejs/tests/routes/persistence.js
index e5d8390..4d25950 100644
--- a/modules/web-control-center/nodejs/tests/routes/persistence.js
+++ b/modules/web-control-center/nodejs/tests/routes/persistence.js
@@ -1,7 +1,7 @@
 var request = require('supertest'),
     should = require('should'),
     express = require('express'),
-    persistenceRouter = require('../../routes/persistences');
+    persistenceRouter = require('../../routes/persistence');
 
 var app = express();
 

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/views/configuration/metadata.jade
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/views/configuration/metadata.jade b/modules/web-control-center/nodejs/views/configuration/metadata.jade
new file mode 100644
index 0000000..81c10dd
--- /dev/null
+++ b/modules/web-control-center/nodejs/views/configuration/metadata.jade
@@ -0,0 +1,42 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+extends ../templates/layout-sidebar
+
+append scripts
+    script(src='/metadata-controller.js')
+
+include ../includes/controls
+
+block content
+    .docs-header
+        h1 Create and configure cache type metadata
+        hr
+    .docs-body(ng-controller='metadataController')
+        .links(ng-hide='metadata.length == 0')
+            table.col-sm-12(st-table='metadata')
+                tbody
+                    tr(ng-repeat='row in metadata track by row._id')
+                        td.col-sm-6(ng-class='{active: row._id == selectedItem._id}')
+                            a(ng-click='selectItem(row)') {{$index + 1}}) {{row.name}}
+        button.btn.btn-primary(ng-click='createItem()') Add metadata
+        hr
+        form.form-horizontal(name='inputForm' ng-if='backupItem' novalidate)
+            .panel-body
+                .settings-row(ng-repeat='field in general')
+                    +form-row
+            button#save-btn.btn.btn-primary(ng-disabled='inputForm.$invalid' ng-click='saveItem()') Save
+            button.btn.btn-primary.btn-second(ng-show='backupItem._id' ng-click='removeItem()') Remove

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/views/configuration/persistence.jade
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/views/configuration/persistence.jade b/modules/web-control-center/nodejs/views/configuration/persistence.jade
index b0e23b6..2d23004 100644
--- a/modules/web-control-center/nodejs/views/configuration/persistence.jade
+++ b/modules/web-control-center/nodejs/views/configuration/persistence.jade
@@ -17,7 +17,7 @@
 extends ../templates/layout-sidebar
 
 append scripts
-    script(src='/persistences-controller.js')
+    script(src='/persistence-controller.js')
 
 include ../includes/controls
 

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/5e41cfa5/modules/web-control-center/nodejs/views/templates/layout-sidebar.jade
----------------------------------------------------------------------
diff --git a/modules/web-control-center/nodejs/views/templates/layout-sidebar.jade b/modules/web-control-center/nodejs/views/templates/layout-sidebar.jade
index 48f0ee5..6e4a008 100644
--- a/modules/web-control-center/nodejs/views/templates/layout-sidebar.jade
+++ b/modules/web-control-center/nodejs/views/templates/layout-sidebar.jade
@@ -16,34 +16,31 @@
 
 extends layout
 
+mixin item(ref, num, txt)
+    li
+        a(ng-class='{active: isActive("#{ref}")}' href='#{ref}')
+            span.fa-stack
+                i.fa.fa-circle-thin.fa-stack-2x
+                i.fa.fa-stack-1x #{num}
+            | #{txt}
+
+mixin subitem(ref, txt)
+    li
+        a(style='font-size: 16px' ng-class='{active: isActive("#{ref}")}' href='#{ref}')
+            span.fa-stack
+                i.fa.fa-stack-2x
+            | #{txt}
+
 block container
     .row
         .col-sm-2.border-right.section-left.greedy
             .sidebar-nav
                 ul.menu(ng-controller='activeLink')
-                    li
-                        a(ng-class="{active: isActive('/configuration/clusters')}" href='/configuration/clusters')
-                            span.fa-stack
-                                i.fa.fa-circle-thin.fa-stack-2x
-                                i.fa.fa-stack-1x 1
-                            | Clusters
-                    li
-                        a(ng-class="{active: isActive('/configuration/caches')}" href='/configuration/caches')
-                            span.fa-stack
-                                i.fa.fa-circle-thin.fa-stack-2x
-                                i.fa.fa-stack-1x 2
-                            | Caches
-
-                    //li
-                    //    a(ng-class="{active: isActive('/persistence')}" href='/persistence') Persistence
-                    //li
-                    //    a(ng-class="{active: isActive('/clients')}" href='/clients') Clients
-                    li
-                        a(ng-class="{active: isActive('/configuration/summary')}" href='/configuration/summary')
-                            span.fa-stack
-                                i.fa.fa-circle-thin.fa-stack-2x
-                                i.fa.fa-stack-1x 3
-                            | Summary
+                    +item('/configuration/clusters', 1, 'Clusters')
+                    +item('/configuration/caches', 2, 'Caches')
+                    //+subitem('/configuration/metadata', 'Metadata')
+                    //+subitem('/configuration/persistence', 'Persistence')
+                    +item('/configuration/summary', 3, 'Summary')
 
         .col-sm-10.border-left.section-right
             .docs-content