http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/controllers/models/metadata.json
----------------------------------------------------------------------
diff --git 
a/modules/control-center-web/src/main/js/controllers/models/metadata.json 
b/modules/control-center-web/src/main/js/controllers/models/metadata.json
new file mode 100644
index 0000000..7ef0e20
--- /dev/null
+++ b/modules/control-center-web/src/main/js/controllers/models/metadata.json
@@ -0,0 +1,279 @@
+{
+  "screenTip": {
+    "workflowTitle": "On This Screen:",
+    "workflowContent": [
+      "Manually enter Metadata",
+      "Load Metadata from Database",
+      "more-info"
+    ],
+    "whatsNextTitle": "Next Steps:",
+    "whatsNextContent": [
+      "Continue to <a href='/configuration/summary'>Summary</a>",
+      "Back to <a href='/configuration/caches'>Caches</a>",
+      "Back to <a href='/configuration/clusters'>Clusters</a>"
+    ]
+  },
+  "moreInfo": {
+    "title": "Metadata page",
+    "content": ["Manage you type metadata on current page.",
+      "Metadata can be assigned to specified <a 
href='/configuration/caches'>caches</a>.",
+      "Generated cluster with caches with metadata configuration available on 
<a href='/configuration/summary'>summary</a> page."]
+  },
+  "metadata": [
+    {
+      "label": "Metadata common",
+      "group": "general",
+      "tip": [
+        "Metadata properties common to Query and Store"
+      ],
+      "fields": [
+        {
+          "label": "Caches",
+          "id": "caches",
+          "type": "dropdown-multiple",
+          "model": "caches",
+          "placeholder": "Choose caches",
+          "placeholderEmpty": "No caches configured",
+          "items": "caches",
+          "tip": [
+            "Select caches to associate database with cache"
+          ],
+          "addLink": {
+            "label": "Add cache(s)",
+            "ref": "/configuration/caches?new"
+          }
+        },
+        {
+          "label": "Key type",
+          "id": "keyType",
+          "type": "typeahead",
+          "items": "javaBuildInClasses",
+          "model": "keyType",
+          "required": true,
+          "placeholder": "Full class name for Key",
+          "tip": [
+            "Key class used to store key in cache"
+          ]
+        },
+        {
+          "label": "Value type",
+          "id": "valueType",
+          "type": "text",
+          "model": "valueType",
+          "required": true,
+          "placeholder": "Full class name for Value",
+          "tip": [
+            "Value class used to store value in cache"
+          ]
+        }
+      ]
+    },
+    {
+      "label": "Metadata for SQL query",
+      "group": "query",
+      "tip": [
+        "Metadata properties for fields queries"
+      ],
+      "fields": [
+        {
+          "label": "Not indexed fields",
+          "id": "queryFields",
+          "ui": "table-pair",
+          "type": "queryFieldsFirst",
+          "model": "queryFields",
+          "keyName": "name",
+          "valueName": "className",
+          "focusId": "QryField",
+          "addTip": "Add not indexed field to query",
+          "removeTip": "Remove field",
+          "tip": [
+            "Collection of name-to-type mappings to be queried, in addition to 
indexed fields"
+          ]
+        },
+        {
+          "label": "Ascending indexed fields",
+          "id": "ascendingFields",
+          "ui": "table-pair",
+          "type": "queryFields",
+          "model": "ascendingFields",
+          "keyName": "name",
+          "valueName": "className",
+          "focusId": "AscField",
+          "addTip": "Add field to index in ascending order",
+          "removeTip": "Remove field",
+          "tip": [
+            "Collection of name-to-type mappings to index in ascending order"
+          ]
+        },
+        {
+          "label": "Descending indexed fields",
+          "id": "descendingFields",
+          "ui": "table-pair",
+          "type": "queryFields",
+          "model": "descendingFields",
+          "keyName": "name",
+          "valueName": "className",
+          "focusId": "DescField",
+          "addTip": "Add field to index in descending order",
+          "removeTip": "Remove field",
+          "tip": [
+            "Collection of name-to-type mappings to index in descending order"
+          ]
+        },
+        {
+          "label": "Text indexed fields",
+          "id": "textFields",
+          "type": "table-simple",
+          "model": "textFields",
+          "placeholder": "Field name",
+          "focusId": "TextField",
+          "addTip": "Add field to index as text",
+          "removeTip": "Remove field",
+          "tableTip": [
+            "Fields to index as text"
+          ],
+          "tip": [
+            "Field to index as text"
+          ]
+        },
+        {
+          "label": "Group indexes",
+          "id": "groups",
+          "type": "table-query-groups",
+          "model": "groups",
+          "addTip": "Add new group",
+          "removeTip": "Remove group",
+          "addItemTip": "Add new field to group",
+          "removeItemTip": "Remove field from group",
+          "tip": [
+            "Collection of group indexes"
+          ]
+        }
+      ]
+    },
+    {
+      "label": "Metadata for cache store",
+      "group": "store",
+      "tip": [
+        "Metadata properties for binding database with cache via POJO cache 
store"
+      ],
+      "fields": [
+        {
+          "label": "Database schema",
+          "id": "databaseSchema",
+          "type": "text",
+          "model": "databaseSchema",
+          "placeholder": "Input DB schema name",
+          "tip": [
+            "Schema name in database"
+          ]
+        },
+        {
+          "label": "Database table",
+          "id": "databaseTable",
+          "type": "text",
+          "model": "databaseTable",
+          "placeholder": "Input DB table name",
+          "tip": [
+            "Table name in database"
+          ]
+        },
+        {
+          "label": "Key fields",
+          "id": "keyFields",
+          "type": "table-db-fields",
+          "model": "keyFields",
+          "keyName": "name",
+          "valueName": "className",
+          "hide": "isJavaBuildInClass()",
+          "focusId": "KeyField",
+          "addTip": "Add key field",
+          "removeTip": "Remove key field",
+          "tip": [
+            "Collection of key fields descriptions for CacheJdbcPojoStore"
+          ]
+        },
+        {
+          "label": "Value fields",
+          "id": "valueFields",
+          "type": "table-db-fields",
+          "model": "valueFields",
+          "keyName": "name",
+          "valueName": "className",
+          "focusId": "ValueField",
+          "addTip": "Add value field",
+          "removeTip": "Remove value field",
+          "tip": [
+            "Collection of value fields descriptions for CacheJdbcPojoStore"
+          ]
+        }
+      ]
+    }
+  ],
+  "metadataDb": [
+    {
+      "label": "Driver JAR",
+      "id": "jdbcDriverJar",
+      "type": "dropdown",
+      "container": "false",
+      "model": "jdbcDriverJar",
+      "items": "jdbcDriverJars",
+      "tip": [
+        "Select appropriate JAR with JDBC driver",
+        "To add another driver you need to place it into '/drivers' folder of 
Ignite Web Agent",
+        "Refer to Ignite Web Agent README.txt for for more information"
+      ]
+    },
+    {
+      "label": "JDBC Driver",
+      "id": "jdbcDriverClass",
+      "type": "text",
+      "model": "jdbcDriverClass",
+      "placeholder": "Full class name of JDBC driver",
+      "tip": [
+        "Full class name of JDBC driver that will be used to connect to 
database"
+      ]
+    },
+    {
+      "label": "JDBC URL",
+      "id": "jdbcUrl",
+      "type": "text",
+      "model": "jdbcUrl",
+      "placeholder": "JDBC URL",
+      "tip": [
+        "JDBC URL for connecting to database.",
+        "Refer to your database documentation for details"
+      ]
+    },
+    {
+      "label": "User",
+      "id": "user",
+      "type": "text",
+      "model": "user",
+      "tip": [
+        "User name for connecting to database"
+      ]
+    },
+    {
+      "label": "Password",
+      "id": "password",
+      "type": "password",
+      "model": "password",
+      "onEnter": "loadMetadataNext()",
+      "tip": [
+        "Password for connecting to database",
+        "Note, password would not be saved"
+      ]
+    },
+    {
+      "label": "Tables only",
+      "id": "tablesOnly",
+      "type": "check",
+      "model": "tablesOnly",
+      "tip": [
+        "If selected then only tables metadata will be parsed",
+        "Otherwise table and view metadata will be parsed"
+      ]
+    }
+  ]
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/controllers/models/summary.json
----------------------------------------------------------------------
diff --git 
a/modules/control-center-web/src/main/js/controllers/models/summary.json 
b/modules/control-center-web/src/main/js/controllers/models/summary.json
new file mode 100644
index 0000000..88ad6b4
--- /dev/null
+++ b/modules/control-center-web/src/main/js/controllers/models/summary.json
@@ -0,0 +1,172 @@
+{
+  "screenTip": {
+    "workflowTitle": "On This Screen:",
+    "workflowContent": [
+      "Download XML Config",
+      "Download Java Code",
+      "Download Docker File",
+      "more-info"
+    ],
+    "whatsNextTitle": "Next Steps:",
+    "whatsNextContent": [
+      "Deploy Ignite Servers",
+      "Connect Ignite Clients",
+      "Analyze with SQL"
+    ]
+  },
+  "moreInfo": {
+    "title": "Summary page",
+    "content": ["Generated cluster's configuration showed on this page.",
+      "Configurations available in XML, Java and Dockerfile format for Server 
and Client mode.",
+      "Database table POJO classes for cluster's metadatas available on 
\"POJO\" tab.",
+      "Use \"Download\" button to receive configurations in ZIP file.",
+      "Go back to change configuration on <a 
href='/configuration/clusters'>clusters</a>, <a 
href='/configuration/caches'>caches</a> or <a 
href='/configuration/metadata'>metadata</a> pages."
+    ]
+  },
+  "clientFields": [
+    {
+      "label": "Near cache start size",
+      "type": "number",
+      "path": "nearConfiguration",
+      "model": "nearStartSize",
+      "placeholder": 375000,
+      "tip": [
+        "Initial cache size for near cache which will be used to pre-create 
internal hash table after start"
+      ]
+    },
+    {
+      "label": "Near cache eviction policy",
+      "type": "dropdown-details",
+      "settings": true,
+      "path": "nearConfiguration.nearEvictionPolicy",
+      "model": "kind",
+      "placeholder": "Choose eviction policy",
+      "items": "evictionPolicies",
+      "tip": [
+        "Cache expiration policy"
+      ],
+      "details": {
+        "LRU": {
+          "expanded": false,
+          "fields": [
+            {
+              "label": "Batch size",
+              "type": "number",
+              "path": "nearConfiguration.nearEvictionPolicy.LRU",
+              "model": "batchSize",
+              "placeholder": 1,
+              "tip": [
+                "Number of entries to remove on shrink"
+              ]
+            },
+            {
+              "label": "Max memory size",
+              "type": "number",
+              "path": "nearConfiguration.nearEvictionPolicy.LRU",
+              "model": "maxMemorySize",
+              "placeholder": 0,
+              "tip": [
+                "Maximum allowed cache size in bytes"
+              ]
+            },
+            {
+              "label": "Max size",
+              "type": "number",
+              "path": "nearConfiguration.nearEvictionPolicy.LRU",
+              "model": "maxSize",
+              "placeholder": 100000,
+              "tip": [
+                "Maximum allowed size of cache before entry will start getting 
evicted"
+              ]
+            }
+          ]
+        },
+        "RND": {
+          "expanded": false,
+          "fields": [
+            {
+              "label": "Max size",
+              "type": "number",
+              "path": "nearConfiguration.nearEvictionPolicy.RND",
+              "model": "maxSize",
+              "placeholder": 100000,
+              "tip": [
+                "Maximum allowed size of cache before entry will start getting 
evicted"
+              ]
+            }
+          ]
+        },
+        "FIFO": {
+          "expanded": false,
+          "fields": [
+            {
+              "label": "Batch size",
+              "type": "number",
+              "path": "nearConfiguration.nearEvictionPolicy.FIFO",
+              "model": "batchSize",
+              "placeholder": 1,
+              "tip": [
+                "Number of entries to remove on shrink"
+              ]
+            },
+            {
+              "label": "Max memory size",
+              "type": "number",
+              "path": "nearConfiguration.nearEvictionPolicy.FIFO",
+              "model": "maxMemorySize",
+              "placeholder": 0,
+              "tip": [
+                "Maximum allowed cache size in bytes"
+              ]
+            },
+            {
+              "label": "Max size",
+              "type": "number",
+              "path": "nearConfiguration.nearEvictionPolicy.FIFO",
+              "model": "maxSize",
+              "placeholder": 100000,
+              "tip": [
+                "Maximum allowed size of cache before entry will start getting 
evicted"
+              ]
+            }
+          ]
+        },
+        "SORTED": {
+          "expanded": false,
+          "fields": [
+            {
+              "label": "Batch size",
+              "type": "number",
+              "path": "nearConfiguration.nearEvictionPolicy.SORTED",
+              "model": "batchSize",
+              "placeholder": 1,
+              "tip": [
+                "Number of entries to remove on shrink"
+              ]
+            },
+            {
+              "label": "Max memory size",
+              "type": "number",
+              "path": "nearConfiguration.nearEvictionPolicy.SORTED",
+              "model": "maxMemorySize",
+              "placeholder": 0,
+              "tip": [
+                "Maximum allowed cache size in bytes"
+              ]
+            },
+            {
+              "label": "Max size",
+              "type": "number",
+              "path": "nearConfiguration.nearEvictionPolicy.SORTED",
+              "model": "maxSize",
+              "placeholder": 100000,
+              "tip": [
+                "Maximum allowed size of cache before entry will start getting 
evicted"
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ]
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/controllers/profile-controller.js
----------------------------------------------------------------------
diff --git 
a/modules/control-center-web/src/main/js/controllers/profile-controller.js 
b/modules/control-center-web/src/main/js/controllers/profile-controller.js
new file mode 100644
index 0000000..5d4567b
--- /dev/null
+++ b/modules/control-center-web/src/main/js/controllers/profile-controller.js
@@ -0,0 +1,94 @@
+/*
+ *
+ *  * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  * contributor license agreements.  See the NOTICE file distributed with
+ *  * this work for additional information regarding copyright ownership.
+ *  * The ASF licenses this file to You under the Apache License, Version 2.0
+ *  * (the "License"); you may not use this file except in compliance with
+ *  * the License.  You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+// Controller for Profile screen.
+consoleModule.controller('profileController',
+    ['$scope', '$http', '$common', '$focus', '$confirm', function ($scope, 
$http, $common, $focus, $confirm) {
+    $scope.profileUser = angular.copy($scope.user);
+
+    if ($scope.profileUser && !$scope.profileUser.token)
+        $scope.profileUser.token = 'No security token. Regenerate please.';
+
+    $scope.generateToken = function () {
+        $confirm.confirm('Are you sure you want to change security token?')
+            .then(function () {
+                $scope.profileUser.token = $commonUtils.randomString(20);
+            })
+    };
+
+    $scope.profileChanged = function () {
+        var old = $scope.user;
+        var cur = $scope.profileUser;
+
+        return old.username != cur.username || old.email != cur.email || 
old.token != cur.token ||
+            (cur.changePassword && !$common.isEmptyString(cur.newPassword));
+    };
+
+    $scope.profileCouldBeSaved = function () {
+        return $scope.profileForm.$valid && $scope.profileChanged();
+    };
+
+    $scope.saveBtnTipText = function () {
+        if (!$scope.profileForm.$valid)
+            return 'Invalid profile settings';
+
+        return $scope.profileChanged() ? 'Save profile' : 'Nothing to save';
+    };
+
+    $scope.saveUser = function () {
+        var profile = $scope.profileUser;
+
+        if (profile) {
+            var userName = profile.username;
+            var changeUsername = userName != $scope.user.username;
+
+            var email = profile.email;
+            var changeEmail = email != $scope.user.email;
+
+            var token = profile.token;
+            var changeToken = token != $scope.user.token;
+
+            if (changeUsername || changeEmail || changeToken || 
profile.changePassword) {
+                $http.post('/profile/save', {
+                    _id: profile._id,
+                    userName: changeUsername ? userName : undefined,
+                    email: changeEmail ? email : undefined,
+                    token: changeToken ? token : undefined,
+                    newPassword: profile.changePassword ? profile.newPassword 
: undefined
+                }).success(function (user) {
+                    $common.showInfo('Profile saved.');
+
+                    profile.changePassword = false;
+                    profile.newPassword = null;
+                    profile.confirmPassword = null;
+
+                    if (changeUsername)
+                        $scope.user.username = userName;
+
+                    if (changeEmail)
+                        $scope.user.email = email;
+
+                    $focus('profile-username');
+                }).error(function (err) {
+                    $common.showError('Failed to save profile: ' + 
$common.errorMessage(err));
+                });
+            }
+        }
+    };
+}]);

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/controllers/sql-controller.js
----------------------------------------------------------------------
diff --git 
a/modules/control-center-web/src/main/js/controllers/sql-controller.js 
b/modules/control-center-web/src/main/js/controllers/sql-controller.js
new file mode 100644
index 0000000..72afcc9
--- /dev/null
+++ b/modules/control-center-web/src/main/js/controllers/sql-controller.js
@@ -0,0 +1,1097 @@
+/*
+ *
+ *  * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  * contributor license agreements.  See the NOTICE file distributed with
+ *  * this work for additional information regarding copyright ownership.
+ *  * The ASF licenses this file to You under the Apache License, Version 2.0
+ *  * (the "License"); you may not use this file except in compliance with
+ *  * the License.  You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+// Controller for SQL notebook screen.
+consoleModule.controller('sqlController',
+    ['$scope', '$window','$controller', '$http', '$timeout', '$common', 
'$confirm', '$interval', '$popover', '$loading',
+    function ($scope, $window, $controller, $http, $timeout, $common, 
$confirm, $interval, $popover, $loading) {
+    // Initialize the super class and extend it.
+    angular.extend(this, $controller('agent-download', {$scope: $scope}));
+
+    $scope.agentGoal = 'execute sql statements';
+    $scope.agentTestDriveOption = '--test-drive-sql';
+
+    $scope.joinTip = $common.joinTip;
+
+    $scope.caches = [];
+
+    $scope.pageSizes = [50, 100, 200, 400, 800, 1000];
+
+    $scope.modes = $common.mkOptions(['PARTITIONED', 'REPLICATED', 'LOCAL']);
+
+    $scope.timeUnit = [
+        {value: 1000, label: 'seconds', short: 's'},
+        {value: 60000, label: 'minutes', short: 'm'},
+        {value: 3600000, label: 'hours', short: 'h'}
+    ];
+
+    $scope.exportDropdown = [{ 'text': 'Export all', 'click': 
'exportAll(paragraph)'}];
+
+    $scope.treeOptions = {
+        nodeChildren: 'children',
+        dirSelectable: true,
+        injectClasses: {
+            iExpanded: 'fa fa-minus-square-o',
+            iCollapsed: 'fa fa-plus-square-o'
+        }
+    };
+
+    var TIME_LINE = {value: -1, type: 'java.sql.Date', label: 'TIME_LINE'};
+
+    var chartHistory = [];
+
+    var HISTORY_LENGTH = 100;
+
+    $scope.chartRemoveKeyColumn = function (paragraph, index) {
+        paragraph.chartKeyCols.splice(index, 1);
+
+        _chartApplySettings(paragraph, true);
+    };
+
+    $scope.chartRemoveValColumn = function (paragraph, index) {
+        paragraph.chartValCols.splice(index, 1);
+
+        _chartApplySettings(paragraph, true);
+    };
+
+    $scope.chartAcceptKeyColumn = function(paragraph, item) {
+        var accepted = !_.includes(paragraph.chartKeyCols, item);
+
+        if (accepted) {
+            paragraph.chartKeyCols = [item];
+
+            _chartApplySettings(paragraph, true);
+        }
+
+        return false;
+    };
+
+    $scope.chartAcceptValColumn = function(paragraph, item) {
+        var accepted = !_.includes(paragraph.chartValCols, item) && item != 
TIME_LINE && _numberType(item.type);
+
+        if (accepted) {
+            paragraph.chartValCols.push(item);
+
+            _chartApplySettings(paragraph, true);
+        }
+
+        return false;
+    };
+
+    var _hideColumn = function (col) {
+        return !(col.fieldName === '_KEY') && !(col.fieldName == '_VAL');
+    };
+
+    var _allColumn = function () {
+        return true;
+    };
+
+    var paragraphId = 0;
+
+    function enhanceParagraph(paragraph) {
+        paragraph.nonEmpty = function () {
+            return this.rows && this.rows.length > 0;
+        };
+
+        paragraph.chart = function () {
+            return this.result != 'table' && this.result != 'none';
+        };
+
+        paragraph.queryExecute = function () {
+            return this.queryArgs && this.queryArgs.type == 'QUERY';
+        };
+
+        paragraph.table = function () {
+            return this.result == 'table';
+        };
+
+        paragraph.chartColumnsConfigured = function () {
+            return !$common.isEmptyArray(this.chartKeyCols) && 
!$common.isEmptyArray(this.chartValCols);
+        };
+
+        paragraph.chartTimeLineEnabled = function () {
+            return !$common.isEmptyArray(this.chartKeyCols) && 
angular.equals(this.chartKeyCols[0], TIME_LINE);
+        };
+
+        paragraph.timeLineSupported = function () {
+            return this.result != 'pie';
+        };
+
+        Object.defineProperty(paragraph, 'gridOptions', { value: {
+            enableColResize: true,
+            columnDefs: [],
+            rowData: null
+        }});
+    }
+
+    $scope.aceInit = function (paragraph) {
+        return function (editor) {
+            editor.setAutoScrollEditorIntoView(true);
+            editor.$blockScrolling = Infinity;
+
+            var renderer = editor.renderer;
+
+            renderer.setHighlightGutterLine(false);
+            renderer.setShowPrintMargin(false);
+            renderer.setOption('fontFamily', 'monospace');
+            renderer.setOption('fontSize', '14px');
+            renderer.setOption('minLines', '5');
+            renderer.setOption('maxLines', '15');
+
+            editor.setTheme('ace/theme/chrome');
+
+            Object.defineProperty(paragraph, 'ace', { value: editor });
+        }
+    };
+
+    var _setActiveCache = function () {
+        if ($scope.caches.length > 0)
+            _.forEach($scope.notebook.paragraphs, function (paragraph) {
+                if (!paragraph.cacheName || !_.find($scope.caches, {name: 
paragraph.cacheName}))
+                    paragraph.cacheName = $scope.caches[0].name;
+            });
+    };
+
+    var loadNotebook = function () {
+        $http.post('/notebooks/get', {noteId: $scope.noteId})
+            .success(function (notebook) {
+                $scope.notebook = notebook;
+
+                $scope.notebook_name = notebook.name;
+
+                _.forEach(notebook.paragraphs, function (paragraph) {
+                    paragraph.id = paragraphId++;
+
+                    enhanceParagraph(paragraph);
+                });
+
+                if (!notebook.paragraphs || notebook.paragraphs.length == 0)
+                    $scope.addParagraph();
+
+                _setActiveCache();
+            })
+            .error(function (errMsg) {
+                $common.showError(errMsg);
+            });
+    };
+
+    loadNotebook();
+
+    var _saveNotebook = function (f) {
+        $http.post('/notebooks/save', $scope.notebook)
+            .success(f || function() {})
+            .error(function (errMsg) {
+                $common.showError(errMsg);
+            });
+    };
+
+    $scope.renameNotebook = function (name) {
+        if (!name)
+            return;
+
+        if ($scope.notebook.name != name) {
+            $scope.notebook.name = name;
+
+            _saveNotebook(function () {
+                var idx = _.findIndex($scope.$root.notebooks, function (item) {
+                    return item._id == $scope.notebook._id;
+                });
+
+                if (idx >= 0) {
+                    $scope.$root.notebooks[idx].name = name;
+
+                    $scope.$root.rebuildDropdown();
+                }
+
+                $scope.notebook.edit = false;
+            });
+        }
+        else
+            $scope.notebook.edit = false
+    };
+
+    $scope.removeNotebook = function () {
+        $confirm.confirm('Are you sure you want to remove: "' + 
$scope.notebook.name + '"?')
+            .then(function () {
+                $http.post('/notebooks/remove', {_id: $scope.notebook._id})
+                    .success(function () {
+                        var idx = _.findIndex($scope.$root.notebooks, function 
(item) {
+                            return item._id == $scope.notebook._id;
+                        });
+
+                        if (idx >= 0) {
+                            $scope.$root.notebooks.splice(idx, 1);
+
+                            if ($scope.$root.notebooks.length > 0)
+                                $window.location = '/sql/' +
+                                    $scope.$root.notebooks[Math.min(idx,  
$scope.$root.notebooks.length - 1)]._id;
+                            else
+                                $window.location = '/configuration/clusters';
+                        }
+                    })
+                    .error(function (errMsg) {
+                        $common.showError(errMsg);
+                    });
+            });
+    };
+
+    $scope.renameParagraph = function (paragraph, newName) {
+        if (!newName)
+            return;
+
+        if (paragraph.name != newName) {
+            paragraph.name = newName;
+
+            _saveNotebook(function () { paragraph.edit = false; });
+        }
+        else
+            paragraph.edit = false
+    };
+
+    $scope.addParagraph = function () {
+        if (!$scope.notebook.paragraphs)
+            $scope.notebook.paragraphs = [];
+
+        var sz = $scope.notebook.paragraphs.length;
+
+        var paragraph = {
+            id: paragraphId++,
+            name: 'Query' + (sz ==0 ? '' : sz),
+            editor: true,
+            query: '',
+            pageSize: $scope.pageSizes[0],
+            result: 'none',
+            rate: {
+                value: 1,
+                unit: 60000,
+                installed: false
+            }
+        };
+
+        enhanceParagraph(paragraph);
+
+        if ($scope.caches && $scope.caches.length > 0)
+            paragraph.cacheName = $scope.caches[0].name;
+
+        
$scope.notebook.expandedParagraphs.push($scope.notebook.paragraphs.length);
+
+        $scope.notebook.paragraphs.push(paragraph);
+    };
+
+    $scope.setResult = function (paragraph, new_result) {
+        var changed = paragraph.result != new_result;
+
+        paragraph.result = paragraph.result === new_result ? 'none' : 
new_result;
+
+        if (changed) {
+            if (paragraph.chart())
+                _chartApplySettings(paragraph, changed);
+            else
+                setTimeout(function () {
+                    paragraph.gridOptions.api.sizeColumnsToFit();
+                });
+        }
+    };
+
+    $scope.resultEq = function(paragraph, result) {
+        return (paragraph.result === result);
+    };
+
+    $scope.removeParagraph = function(paragraph) {
+        $confirm.confirm('Are you sure you want to remove: "' + paragraph.name 
+ '"?')
+            .then(function () {
+                    var paragraph_idx = 
_.findIndex($scope.notebook.paragraphs, function (item) {
+                        return paragraph == item;
+                    });
+
+                    var panel_idx = 
_.findIndex($scope.notebook.expandedParagraphs, function (item) {
+                        return paragraph_idx == item;
+                    });
+
+                    if (panel_idx >= 0)
+                        $scope.notebook.expandedParagraphs.splice(panel_idx, 
1);
+
+                    $scope.notebook.paragraphs.splice(paragraph_idx, 1);
+            });
+    };
+
+    function getTopology(onSuccess, onException) {
+        $http.post('/agent/topology')
+            .success(function (caches) {
+                onSuccess();
+
+                var oldCaches = $scope.caches;
+
+                $scope.caches = _.sortBy(caches, 'name');
+
+                _.forEach(caches, function (cache) {
+                    var old = _.find(oldCaches, { name: cache.name });
+
+                    if (old && old.metadata)
+                        cache.metadata = old.metadata;
+                });
+
+                _setActiveCache();
+            })
+            .error(function (err, status) {
+                onException(err, status);
+            });
+    }
+
+    $scope.checkNodeConnection(getTopology);
+
+    var _columnFilter = function(paragraph) {
+        return paragraph.disabledSystemColumns || paragraph.systemColumns ? 
_allColumn : _hideColumn;
+    };
+
+    var _notObjectType = function(cls) {
+        return $common.isJavaBuildInClass(cls);
+    };
+
+    var _numberClasses = ['java.math.BigDecimal', 'java.lang.Byte', 
'java.lang.Double',
+        'java.lang.Float', 'java.lang.Integer', 'java.lang.Long', 
'java.lang.Short'];
+
+    var _numberType = function(cls) {
+        return _.contains(_numberClasses, cls);
+    };
+
+    var _rebuildColumns = function (paragraph) {
+        var columnDefs = [];
+
+        _.forEach(paragraph.meta, function (meta, idx) {
+            if (paragraph.columnFilter(meta)) {
+                if (_notObjectType(meta.fieldTypeName))
+                    paragraph.chartColumns.push({value: idx, type: 
meta.fieldTypeName, label: meta.fieldName});
+
+                // Index for explain, execute and fieldName for scan.
+                var colValue = 'data[' +  (paragraph.queryArgs.query ? idx : 
'"' + meta.fieldName + '"') + ']';
+
+                columnDefs.push({
+                    headerName: meta.fieldName,
+                    valueGetter: 
$common.isJavaBuildInClass(meta.fieldTypeName) ? colValue : 'JSON.stringify(' + 
colValue + ')'
+                });
+            }
+        });
+
+        paragraph.gridOptions.api.setColumnDefs(columnDefs);
+
+        if (paragraph.chartColumns.length > 0)
+            paragraph.chartColumns.push(TIME_LINE);
+
+        // We could accept onl not object columns for X axis.
+        paragraph.chartKeyCols = _retainColumns(paragraph.chartColumns, 
paragraph.chartKeyCols, _notObjectType);
+
+        // We could accept only numeric columns for Y axis.
+        paragraph.chartValCols = _retainColumns(paragraph.chartColumns, 
paragraph.chartValCols, _numberType, paragraph.chartKeyCols[0]);
+    };
+
+    $scope.toggleSystemColumns = function (paragraph) {
+        if (paragraph.disabledSystemColumns)
+            return;
+
+        paragraph.systemColumns = !paragraph.systemColumns;
+
+        paragraph.columnFilter = _columnFilter(paragraph);
+
+        paragraph.chartColumns = [];
+
+        _rebuildColumns(paragraph);
+
+        setTimeout(function () {
+            paragraph.gridOptions.api.sizeColumnsToFit();
+        });
+    };
+
+    function _retainColumns(allCols, curCols, acceptableType, dfltCol) {
+        var retainedCols = [];
+
+        var allColsLen = allCols.length;
+
+        if (allColsLen > 0) {
+            curCols.forEach(function (curCol) {
+                var col = _.find(allCols, {label: curCol.label});
+
+                if (col && acceptableType(col.type))
+                    retainedCols.push(col);
+            });
+
+            if ($common.isEmptyArray(retainedCols))
+                for (idx = 0; idx < allColsLen; idx++) {
+                    var col = allCols[idx];
+
+                    if (acceptableType(col.type) && col != dfltCol) {
+                        retainedCols.push(col);
+
+                        break;
+                    }
+                }
+
+            if ($common.isEmptyArray(retainedCols) && dfltCol && 
acceptableType(dfltCol.type))
+                retainedCols.push(dfltCol);
+        }
+
+        return retainedCols;
+    }
+
+    var _processQueryResult = function (paragraph, refreshMode) {
+        return function (res) {
+            if (res.meta && !refreshMode) {
+                paragraph.meta = [];
+
+                paragraph.chartColumns = [];
+
+                if (!$common.isDefined(paragraph.chartKeyCols))
+                    paragraph.chartKeyCols = [];
+
+                if (!$common.isDefined(paragraph.chartValCols ))
+                    paragraph.chartValCols = [];
+
+                if (res.meta.length <= 2) {
+                    var _key = _.find(res.meta, {fieldName: '_KEY'});
+                    var _val = _.find(res.meta, {fieldName: '_VAL'});
+
+                    paragraph.disabledSystemColumns = (res.meta.length == 2 && 
_key && _val) ||
+                        (res.meta.length == 1 && (_key || _val));
+                }
+
+                paragraph.columnFilter = _columnFilter(paragraph);
+
+                paragraph.meta = res.meta;
+
+                _rebuildColumns(paragraph);
+            }
+
+            paragraph.page = 1;
+
+            paragraph.total = 0;
+
+            paragraph.queryId = res.queryId;
+
+            delete paragraph.errMsg;
+
+            // Prepare explain results for display in table.
+            if (paragraph.queryArgs.type == "EXPLAIN" && res.rows) {
+                paragraph.rows = [];
+
+                res.rows.forEach(function (row, i) {
+                    var line = res.rows.length - 1 == i ? row[0] : row[0] + 
'\n';
+
+                    line.replace(/\"/g, '').split('\n').forEach(function 
(line) {
+                        paragraph.rows.push([line]);
+                    });
+                });
+            }
+            else
+                paragraph.rows = res.rows;
+
+            paragraph.gridOptions.api.setRowData(paragraph.rows);
+
+            if (!refreshMode)
+                setTimeout(function () { 
paragraph.gridOptions.api.sizeColumnsToFit(); });
+
+            // Add results to history.
+            chartHistory.push({tm: new Date(), rows: paragraph.rows});
+
+            if (chartHistory.length > HISTORY_LENGTH)
+                chartHistory.shift();
+
+            _showLoading(paragraph, false);
+
+            if (paragraph.result == 'none' || paragraph.queryArgs.type != 
"QUERY")
+                paragraph.result = 'table';
+            else if (paragraph.chart())
+                _chartApplySettings(paragraph);
+        }
+    };
+
+    var _executeRefresh = function (paragraph) {
+        $http.post('/agent/query', paragraph.queryArgs)
+            .success(_processQueryResult(paragraph, true))
+            .error(function (errMsg) {
+                paragraph.errMsg = errMsg;
+            });
+    };
+
+    var _showLoading = function (paragraph, enable) {
+        if (paragraph.table())
+            paragraph.gridOptions.api.showLoading(enable);
+
+        paragraph.loading = enable;
+    };
+
+    $scope.execute = function (paragraph) {
+        _saveNotebook();
+
+        paragraph.queryArgs = { type: "QUERY", query: paragraph.query, 
pageSize: paragraph.pageSize, cacheName: paragraph.cacheName };
+
+        _showLoading(paragraph, true);
+
+        $http.post('/agent/query', paragraph.queryArgs)
+            .success(function (res) {
+                _processQueryResult(paragraph)(res);
+
+                _tryStartRefresh(paragraph);
+            })
+            .error(function (errMsg) {
+                paragraph.errMsg = errMsg;
+
+                _showLoading(paragraph, false);
+
+                $scope.stopRefresh(paragraph);
+            });
+
+        paragraph.ace.focus();
+    };
+
+    $scope.explain = function (paragraph) {
+        _saveNotebook();
+
+        _cancelRefresh(paragraph);
+
+        paragraph.queryArgs = { type: "EXPLAIN", query: 'EXPLAIN ' + 
paragraph.query, pageSize: paragraph.pageSize, cacheName: paragraph.cacheName };
+
+        _showLoading(paragraph, true);
+
+        $http.post('/agent/query', paragraph.queryArgs)
+            .success(_processQueryResult(paragraph))
+            .error(function (errMsg) {
+                paragraph.errMsg = errMsg;
+
+                _showLoading(paragraph, false);
+            });
+
+        paragraph.ace.focus();
+    };
+
+    $scope.scan = function (paragraph) {
+        _saveNotebook();
+
+        _cancelRefresh(paragraph);
+
+        paragraph.queryArgs = { type: "SCAN", pageSize: paragraph.pageSize, 
cacheName: paragraph.cacheName };
+
+        _showLoading(paragraph, true);
+
+        $http.post('/agent/scan', paragraph.queryArgs)
+            .success(_processQueryResult(paragraph))
+            .error(function (errMsg) {
+                paragraph.errMsg = errMsg;
+
+                _showLoading(paragraph, false);
+            });
+
+        paragraph.ace.focus();
+    };
+
+    $scope.nextPage = function(paragraph) {
+        _showLoading(paragraph, true);
+
+        $http.post('/agent/query/fetch', {queryId: paragraph.queryId, 
pageSize: paragraph.pageSize, cacheName: paragraph.queryArgs.cacheName})
+            .success(function (res) {
+                paragraph.page++;
+
+                paragraph.total += paragraph.rows.length;
+
+                paragraph.rows = res.rows;
+
+                paragraph.gridOptions.api.setRowData(res.rows);
+
+                _showLoading(paragraph, false);
+
+                if (res.last)
+                    delete paragraph.queryId;
+            })
+            .error(function (errMsg) {
+                paragraph.errMsg = errMsg;
+
+                _showLoading(paragraph, false);
+            });
+    };
+
+    var _export = function(fileName, meta, rows) {
+        var csvContent = '';
+
+        if (meta) {
+            csvContent += meta.map(function (col) {
+                var res = [];
+
+                if (col.schemaName)
+                    res.push(col.schemaName);
+                if (col.typeName)
+                    res.push(col.typeName);
+
+                res.push(col.fieldName);
+
+                return res.join('.');
+            }).join(',') + '\n';
+        }
+
+        rows.forEach(function (row) {
+            if (Array.isArray(row)) {
+                csvContent += row.map(function (elem) {
+                    return elem ? JSON.stringify(elem) : '';
+                }).join(',');
+            }
+            else {
+                var first = true;
+
+                meta.forEach(function (prop) {
+                    if (first)
+                        first = false;
+                    else
+                        csvContent += ',';
+
+                    var elem = row[prop.fieldName];
+
+                    csvContent += elem ? JSON.stringify(elem) : '';
+                });
+            }
+
+            csvContent += '\n';
+        });
+
+        $common.download('application/octet-stream;charset=utf-8', fileName, 
escape(csvContent));
+    };
+
+    $scope.exportPage = function(paragraph) {
+        _export(paragraph.name + '.csv', paragraph.meta, paragraph.rows);
+    };
+
+    $scope.exportAll = function(paragraph) {
+        $http.post('/agent/query/getAll', {query: paragraph.query, cacheName: 
paragraph.cacheName})
+            .success(function (item) {
+                _export(paragraph.name + '-all.csv', item.meta, item.rows);
+            })
+            .error(function (errMsg) {
+                $common.showError(errMsg);
+            });
+    };
+
+    $scope.rateAsString = function (paragraph) {
+        if (paragraph.rate && paragraph.rate.installed) {
+            var idx = _.findIndex($scope.timeUnit, function (unit) {
+                return unit.value == paragraph.rate.unit;
+            });
+
+            if (idx >= 0)
+                return ' ' + paragraph.rate.value + $scope.timeUnit[idx].short;
+
+            paragraph.rate.installed = false;
+        }
+
+        return '';
+    };
+
+    var _cancelRefresh = function (paragraph) {
+        if (paragraph.rate && paragraph.rate.stopTime) {
+            delete paragraph.queryArgs;
+
+            paragraph.rate.installed = false;
+
+            $interval.cancel(paragraph.rate.stopTime);
+
+            delete paragraph.rate.stopTime;
+        }
+    };
+
+    var _tryStopRefresh = function (paragraph) {
+        if (paragraph.rate && paragraph.rate.stopTime) {
+            $interval.cancel(paragraph.rate.stopTime);
+
+            delete paragraph.rate.stopTime;
+        }
+    };
+
+    var _tryStartRefresh = function (paragraph) {
+        _tryStopRefresh(paragraph);
+
+        if (paragraph.rate && paragraph.rate.installed && paragraph.queryArgs) 
{
+            $scope.chartAcceptKeyColumn(paragraph, TIME_LINE);
+
+            _executeRefresh(paragraph);
+
+            var delay = paragraph.rate.value * paragraph.rate.unit;
+
+            paragraph.rate.stopTime = $interval(_executeRefresh, delay, 0, 
false, paragraph);
+        }
+    };
+
+    $scope.startRefresh = function (paragraph, value, unit) {
+        paragraph.rate.value = value;
+        paragraph.rate.unit = unit;
+        paragraph.rate.installed = true;
+
+        _tryStartRefresh(paragraph);
+    };
+
+    $scope.stopRefresh = function (paragraph) {
+        paragraph.rate.installed = false;
+
+        _tryStopRefresh(paragraph);
+    };
+
+    function _chartNumber(arr, idx, dflt) {
+        if (arr && arr.length > idx && _.isNumber(arr[idx])) {
+            return arr[idx];
+        }
+
+        return dflt;
+    }
+
+    function _chartLabel(arr, idx, dflt) {
+        if (arr && arr.length > idx && _.isString(arr[idx]))
+            return arr[idx];
+
+        return dflt;
+    }
+
+    function _chartDatum(paragraph) {
+        var datum = [];
+
+        if (paragraph.chartColumnsConfigured()) {
+            paragraph.chartValCols.forEach(function (valCol) {
+                var index = 0;
+                var values = [];
+
+                if (paragraph.chartTimeLineEnabled()) {
+                    if (paragraph.charts && paragraph.charts.length == 1)
+                        datum = paragraph.charts[0].data;
+
+                    var chartData = _.find(datum, {key: valCol.label});
+
+                    if (chartData) {
+                        var history = _.last(chartHistory);
+
+                        chartData.values.push({
+                            x: history.tm,
+                            y: _chartNumber(history.rows[0], valCol.value, 
index++)
+                        });
+
+                        if (chartData.length > HISTORY_LENGTH)
+                            chartData.shift();
+                    }
+                    else {
+                        values = _.map(chartHistory, function (history) {
+                            return {
+                                x: history.tm,
+                                y: _chartNumber(history.rows[0], valCol.value, 
index++)
+                            }
+                        });
+
+                        datum.push({key: valCol.label, values: values});
+                    }
+                }
+                else {
+                    values = _.map(paragraph.rows, function (row) {
+                        return {
+                            x: _chartNumber(row, 
paragraph.chartKeyCols[0].value, index),
+                            xLbl: _chartLabel(row, 
paragraph.chartKeyCols[0].value, undefined),
+                            y: _chartNumber(row, valCol.value, index++)
+                        }
+                    });
+
+                    datum.push({key: valCol.label, values: values});
+                }
+            });
+        }
+
+        return datum;
+    }
+
+    function _pieChartDatum(paragraph) {
+        var datum = [];
+
+        if (paragraph.chartColumnsConfigured() && 
!paragraph.chartTimeLineEnabled()) {
+            paragraph.chartValCols.forEach(function (valCol) {
+                var index = 0;
+
+                var values = _.map(paragraph.rows, function (row) {
+                    return {
+                        x: row[paragraph.chartKeyCols[0].value],
+                        y: _chartNumber(row, valCol.value, index++)
+                    }
+                });
+
+                datum.push({key: valCol.label, values: values});
+            });
+        }
+
+        return datum;
+    }
+
+    function _chartApplySettings(paragraph, resetCharts) {
+        if (resetCharts)
+            paragraph.charts = [];
+
+        if (paragraph.chart() && paragraph.nonEmpty()) {
+            switch (paragraph.result) {
+                case 'bar':
+                    _barChart(paragraph);
+                    break;
+
+                case 'pie':
+                    _pieChart(paragraph);
+                    break;
+
+                case 'line':
+                    _lineChart(paragraph);
+                    break;
+
+                case 'area':
+                    _areaChart(paragraph);
+                    break;
+            }
+        }
+    }
+
+    function _colLabel(col) {
+        return col.label;
+    }
+
+    function _chartAxisLabel(cols, dflt) {
+        return $common.isEmptyArray(cols) ? dflt : _.map(cols, 
_colLabel).join(', ');
+    }
+
+    function _xX(d) {
+        return d.x;
+    }
+
+    function _yY(d) {
+        return d.y;
+    }
+
+    function _xAxisTimeFormat(d) {
+        return d3.time.format('%X')(new Date(d));
+    }
+
+    var _xAxisWithLabelFormat = function(values) {
+        return function (d) {
+            var dx = values[d];
+
+            if (!dx)
+                return d3.format(',.2f')(d);
+
+            var lbl = values[d]['xLbl'];
+
+            return lbl ? lbl : d3.format(',.2f')(d);
+        }
+    };
+
+    function _barChart(paragraph) {
+        var datum = _chartDatum(paragraph);
+
+        if ($common.isEmptyArray(paragraph.charts)) {
+            var options = {
+                chart: {
+                    type: 'multiBarChart',
+                    height: 400,
+                    margin: {left: 70},
+                    duration: 0,
+                    x: _xX,
+                    y: _yY,
+                    xAxis: {
+                        axisLabel: _chartAxisLabel(paragraph.chartKeyCols, 
'X'),
+                        tickFormat: paragraph.chartTimeLineEnabled() ? 
_xAxisTimeFormat : _xAxisWithLabelFormat(datum[0].values),
+                        showMaxMin: false
+                    },
+                    yAxis: {
+                        axisLabel:  _chartAxisLabel(paragraph.chartValCols, 
'Y'),
+                        tickFormat: d3.format(',.2f')
+                    },
+                    showControls: true
+                }
+            };
+
+            paragraph.charts = [{options: options, data: datum}];
+
+            $timeout(function () {
+                paragraph.charts[0].api.update();
+            }, 100);
+        }
+        else
+            $timeout(function () {
+                if (paragraph.chartTimeLineEnabled())
+                    paragraph.charts[0].api.update();
+                else
+                    paragraph.charts[0].api.updateWithData(datum);
+            });
+    }
+
+    function _pieChart(paragraph) {
+        var datum = _pieChartDatum(paragraph);
+
+        if (datum.length == 0)
+            datum = [{values: []}];
+
+        paragraph.charts = _.map(datum, function (data) {
+            return {
+                options: {
+                    chart: {
+                        type: 'pieChart',
+                        height: 400,
+                        duration: 0,
+                        x: _xX,
+                        y: _yY,
+                        showLabels: true,
+                        labelThreshold: 0.05,
+                        labelType: 'percent',
+                        donut: true,
+                        donutRatio: 0.35
+                    },
+                    title: {
+                        enable: true,
+                        text: data.key
+                    }
+                },
+                data: data.values
+            }
+        });
+
+        $timeout(function () {
+            paragraph.charts[0].api.update();
+        }, 100);
+    }
+
+    function _lineChart(paragraph) {
+        var datum = _chartDatum(paragraph);
+
+        if ($common.isEmptyArray(paragraph.charts)) {
+            var options = {
+                chart: {
+                    type: 'lineChart',
+                    height: 400,
+                    margin: { left: 70 },
+                    duration: 0,
+                    x: _xX,
+                    y: _yY,
+                    xAxis: {
+                        axisLabel: _chartAxisLabel(paragraph.chartKeyCols, 
'X'),
+                        tickFormat: paragraph.chartTimeLineEnabled() ? 
_xAxisTimeFormat : _xAxisWithLabelFormat(datum[0].values),
+                        showMaxMin: false
+                    },
+                    yAxis: {
+                        axisLabel:  _chartAxisLabel(paragraph.chartValCols, 
'Y'),
+                        tickFormat: d3.format(',.2f')
+                    },
+                    useInteractiveGuideline: true
+                }
+            };
+
+            paragraph.charts = [{options: options, data: datum}];
+
+            $timeout(function () {
+                paragraph.charts[0].api.update();
+            }, 100);
+        }
+        else
+            $timeout(function () {
+                if (paragraph.chartTimeLineEnabled())
+                    paragraph.charts[0].api.update();
+                else
+                    paragraph.charts[0].api.updateWithData(datum);
+            });
+    }
+
+    function _areaChart(paragraph) {
+        var datum = _chartDatum(paragraph);
+
+        if ($common.isEmptyArray(paragraph.charts)) {
+            var options = {
+                chart: {
+                    type: 'stackedAreaChart',
+                    height: 400,
+                    margin: {left: 70},
+                    duration: 0,
+                    x: _xX,
+                    y: _yY,
+                    xAxis: {
+                        axisLabel:  _chartAxisLabel(paragraph.chartKeyCols, 
'X'),
+                        tickFormat: paragraph.chartTimeLineEnabled() ? 
_xAxisTimeFormat : _xAxisWithLabelFormat(datum[0].values),
+                        showMaxMin: false
+                    },
+                    yAxis: {
+                        axisLabel:  _chartAxisLabel(paragraph.chartValCols, 
'Y'),
+                        tickFormat: d3.format(',.2f')
+                    }
+                }
+            };
+
+            paragraph.charts = [{options: options, data: datum}];
+
+            $timeout(function () {
+                paragraph.charts[0].api.update();
+            }, 100);
+        }
+        else
+            $timeout(function () {
+                if (paragraph.chartTimeLineEnabled())
+                    paragraph.charts[0].api.update();
+                else
+                    paragraph.charts[0].api.updateWithData(datum);
+            });
+    }
+
+    $scope.actionAvailable = function (paragraph, needQuery) {
+        return $scope.caches.length > 0 && paragraph.cacheName && (!needQuery 
|| paragraph.query) && !paragraph.loading;
+    };
+
+    $scope.actionTooltip = function (paragraph, action, needQuery) {
+        if ($scope.actionAvailable(paragraph, needQuery))
+            return;
+
+        if (paragraph.loading)
+            return 'Wating for server response';
+
+        return 'To ' + action + ' query select cache' + (needQuery ? ' and 
input query' : '');
+    };
+
+    $scope.clickableMetadata = function (node) {
+        return node.type.slice(0, 5) != 'index';
+    };
+
+    $scope.dblclickMetadata = function (paragraph, node) {
+        paragraph.ace.insert(node.name);
+        var position = paragraph.ace.selection.getCursor();
+
+        paragraph.query = paragraph.ace.getValue();
+
+        setTimeout(function () {
+            paragraph.ace.selection.moveCursorToPosition(position);
+
+            paragraph.ace.focus();
+        }, 1);
+    };
+
+    $scope.tryLoadMetadata = function (cache) {
+        if (!cache.metadata) {
+            $loading.start('loadingCacheMetadata');
+
+            $http.post('/agent/cache/metadata', {cacheName: cache.name})
+                .success(function (metadata) {
+                    cache.metadata = _.sortBy(metadata, 'name');
+                })
+                .error(function (errMsg) {
+                    $common.showError(errMsg);
+                })
+                .finally(function() {
+                    $loading.finish('loadingCacheMetadata');
+                });
+        }
+    }
+}]);

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/controllers/summary-controller.js
----------------------------------------------------------------------
diff --git 
a/modules/control-center-web/src/main/js/controllers/summary-controller.js 
b/modules/control-center-web/src/main/js/controllers/summary-controller.js
new file mode 100644
index 0000000..a3d2dd1
--- /dev/null
+++ b/modules/control-center-web/src/main/js/controllers/summary-controller.js
@@ -0,0 +1,233 @@
+/*
+ *
+ *  * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  * contributor license agreements.  See the NOTICE file distributed with
+ *  * this work for additional information regarding copyright ownership.
+ *  * The ASF licenses this file to You under the Apache License, Version 2.0
+ *  * (the "License"); you may not use this file except in compliance with
+ *  * the License.  You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+// Controller for Summary screen.
+consoleModule.controller('summaryController', [
+    '$scope', '$http', '$common', '$loading', '$message',
+    function ($scope, $http, $common, $loading, $message) {
+    $scope.joinTip = $common.joinTip;
+    $scope.getModel = $common.getModel;
+
+    $scope.showMoreInfo = $message.message;
+
+    $scope.javaClassItems = [
+        {label: 'snippet', value: 1},
+        {label: 'factory class', value: 2}
+    ];
+
+    $scope.evictionPolicies = [
+        {value: 'LRU', label: 'LRU'},
+        {value: 'RND', label: 'Random'},
+        {value: 'FIFO', label: 'FIFO'},
+        {value: 'SORTED', label: 'Sorted'},
+        {value: undefined, label: 'Not set'}
+    ];
+
+    $scope.tabsServer = { activeTab: 0 };
+    $scope.tabsClient = { activeTab: 0 };
+
+    $scope.pojoClasses = function() {
+        var classes = [];
+
+        _.forEach($generatorJava.metadatas, function(meta) {
+            classes.push(meta.keyType);
+            classes.push(meta.valueType);
+        });
+
+        return classes;
+    };
+
+    $scope.oss = ['debian:8', 'ubuntu:14.10'];
+
+    $scope.configServer = {javaClassServer: 1, os: undefined};
+    $scope.configClient = {};
+
+    $scope.backupItem = {javaClassClient: 1};
+
+    $http.get('/models/summary.json')
+        .success(function (data) {
+            $scope.screenTip = data.screenTip;
+            $scope.moreInfo = data.moreInfo;
+            $scope.clientFields = data.clientFields;
+        })
+        .error(function (errMsg) {
+            $common.showError(errMsg);
+        });
+
+    $scope.clusters = [];
+
+    $scope.aceInit = function (editor) {
+        editor.setReadOnly(true);
+        editor.setOption('highlightActiveLine', false);
+        editor.setAutoScrollEditorIntoView(true);
+        editor.$blockScrolling = Infinity;
+
+        var renderer = editor.renderer;
+
+        renderer.setHighlightGutterLine(false);
+        renderer.setShowPrintMargin(false);
+        renderer.setOption('fontSize', '14px');
+        renderer.setOption('minLines', '3');
+        renderer.setOption('maxLines', '50');
+
+        editor.setTheme('ace/theme/chrome');
+    };
+
+    $scope.generateJavaServer = function () {
+        $scope.javaServer = $generatorJava.cluster($scope.selectedItem, 
$scope.configServer.javaClassServer === 2);
+    };
+
+    function selectPojoClass(config) {
+        _.forEach($generatorJava.metadatas, function(meta) {
+            if (meta.keyType == config.pojoClass)
+                return config.pojoClassBody = meta.keyClass;
+
+            if (meta.valueType == config.pojoClass)
+                return config.pojoClassBody = meta.valueClass;
+        });
+    }
+
+    function pojoClsListener(config) {
+        return function () {
+            selectPojoClass(config);
+        };
+    }
+
+    $scope.updatePojos = function() {
+        if ($common.isDefined($scope.selectedItem)) {
+            var curServCls = $scope.configServer.pojoClass;
+            var curCliCls = $scope.configClient.pojoClass;
+
+            $generatorJava.pojos($scope.selectedItem.caches, 
$scope.configServer.useConstructor, $scope.configServer.includeKeyFields);
+
+            function restoreSelected(selected, config, tabs) {
+                if (!$common.isDefined(selected) || 
_.findIndex($generatorJava.metadatas, function (meta) {
+                        return meta.keyType == selected || meta.valueType == 
selected;
+                    }) < 0) {
+                    if ($generatorJava.metadatas.length > 0) {
+                        if 
($common.isDefined($generatorJava.metadatas[0].keyType))
+                            config.pojoClass = 
$generatorJava.metadatas[0].keyType;
+                        else
+                            config.pojoClass = 
$generatorJava.metadatas[0].valueType;
+                    }
+                    else {
+                        config.pojoClass = undefined;
+
+                        if (tabs.activeTab == 2)
+                            tabs.activeTab = 0;
+                    }
+                }
+                else
+                    config.pojoClass = selected;
+
+                selectPojoClass(config);
+            }
+
+            restoreSelected(curServCls, $scope.configServer, 
$scope.tabsServer);
+            restoreSelected(curCliCls, $scope.configClient, $scope.tabsClient);
+        }
+    };
+
+    $scope.$watch('configServer.javaClassServer', $scope.generateJavaServer, 
true);
+
+    $scope.$watch('configServer.pojoClass', 
pojoClsListener($scope.configServer), true);
+    $scope.$watch('configClient.pojoClass', 
pojoClsListener($scope.configClient), true);
+
+    $scope.$watch('configServer.useConstructor', $scope.updatePojos, true);
+
+    $scope.$watch('configServer.includeKeyFields', $scope.updatePojos, true);
+
+    $scope.generateDockerServer = function() {
+        var os = $scope.configServer.os ? $scope.configServer.os : 
$scope.oss[0];
+
+        $scope.dockerServer = 
$generatorDocker.clusterDocker($scope.selectedItem, os);
+    };
+
+    $scope.$watch('configServer.os', $scope.generateDockerServer, true);
+
+    $scope.generateClient = function () {
+        $scope.xmlClient = $generatorXml.cluster($scope.selectedItem, 
$scope.backupItem.nearConfiguration);
+        $scope.javaClient = $generatorJava.cluster($scope.selectedItem, 
$scope.backupItem.javaClassClient === 2,
+            $scope.backupItem.nearConfiguration, 
$scope.configServer.useConstructor);
+    };
+
+    $scope.$watch('backupItem', $scope.generateClient, true);
+
+    $scope.selectItem = function (cluster) {
+        if (!cluster)
+            return;
+
+        $scope.selectedItem = cluster;
+
+        $scope.xmlServer = $generatorXml.cluster(cluster);
+
+        $scope.generateJavaServer();
+
+        $scope.generateDockerServer();
+
+        $scope.generateClient();
+
+        $scope.updatePojos();
+    };
+
+    $scope.pojoAvailable = function() {
+        return $common.isDefined($generatorJava.metadatas) && 
$generatorJava.metadatas.length > 0;
+    };
+
+    $loading.start('loadingSummaryScreen');
+
+    $http.post('clusters/list')
+        .success(function (data) {
+            $scope.clusters = data.clusters;
+
+            if ($scope.clusters.length > 0) {
+                // Populate clusters with caches.
+                _.forEach($scope.clusters, function (cluster) {
+                    cluster.caches = _.filter(data.caches, function (cache) {
+                        return _.contains(cluster.caches, cache._id);
+                    });
+                });
+
+                var restoredId = sessionStorage.summarySelectedId;
+
+                var selectIdx = 0;
+
+                if (restoredId) {
+                    var idx = _.findIndex($scope.clusters, function (cluster) {
+                        return cluster._id == restoredId;
+                    });
+
+                    if (idx >= 0)
+                        selectIdx = idx;
+                    else
+                        delete sessionStorage.summarySelectedId;
+                }
+
+                $scope.selectItem($scope.clusters[selectIdx]);
+
+                $scope.$watch('selectedItem', function (val) {
+                    if (val)
+                        sessionStorage.summarySelectedId = val._id;
+                }, true);
+            }
+        })
+        .finally(function () {
+            $loading.finish('loadingSummaryScreen');
+        });
+}]);

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/db.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/db.js 
b/modules/control-center-web/src/main/js/db.js
new file mode 100644
index 0000000..c9c6b39
--- /dev/null
+++ b/modules/control-center-web/src/main/js/db.js
@@ -0,0 +1,431 @@
+/*
+ *
+ *  * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  * contributor license agreements.  See the NOTICE file distributed with
+ *  * this work for additional information regarding copyright ownership.
+ *  * The ASF licenses this file to You under the Apache License, Version 2.0
+ *  * (the "License"); you may not use this file except in compliance with
+ *  * the License.  You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT 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 config = require('./helpers/configuration-loader.js');
+
+// Mongoose for mongodb.
+var mongoose = require('mongoose'),
+    Schema = mongoose.Schema,
+    ObjectId = mongoose.Schema.Types.ObjectId,
+    passportLocalMongoose = require('passport-local-mongoose');
+
+var deepPopulate = require('mongoose-deep-populate')( mongoose);
+
+// Connect to mongoDB database.
+mongoose.connect(config.get('mongoDB:url'), {server: {poolSize: 4}});
+
+// Define Account schema.
+var AccountSchema = new Schema({
+    username: String,
+    email: String,
+    lastLogin: Date,
+    admin: Boolean,
+    token: String,
+    resetPasswordToken: String
+});
+
+// Install passport plugin.
+AccountSchema.plugin(passportLocalMongoose, {usernameField: 'email', 
limitAttempts: true, lastLoginField: 'lastLogin',
+    usernameLowerCase: true});
+
+// Configure transformation to JSON.
+AccountSchema.set('toJSON', {
+    transform: function(doc, ret) {
+        return {
+            _id: ret._id,
+            email: ret.email,
+            username: ret.username,
+            admin: ret.admin,
+            token: ret.token,
+            lastLogin: ret.lastLogin
+        };
+    }
+});
+
+// Define Account model.
+exports.Account = mongoose.model('Account', AccountSchema);
+
+// Define Space model.
+exports.Space = mongoose.model('Space', new Schema({
+    name: String,
+    owner: {type: ObjectId, ref: 'Account'},
+    usedBy: [{
+        permission: {type: String, enum: ['VIEW', 'FULL']},
+        account: {type: ObjectId, ref: 'Account'}
+    }]
+}));
+
+// Define Cache type metadata schema.
+var CacheTypeMetadataSchema = new Schema({
+    space: {type: ObjectId, ref: 'Space'},
+    caches: [{type: ObjectId, ref: 'Cache'}],
+    kind: {type: String, enum: ['query', 'store', 'both']},
+    databaseSchema: String,
+    databaseTable: String,
+    keyType: String,
+    valueType: String,
+    keyFields: [{databaseName: String, databaseType: String, javaName: String, 
javaType: String}],
+    valueFields: [{databaseName: String, databaseType: String, javaName: 
String, javaType: String}],
+    queryFields: [{name: String, className: String}],
+    ascendingFields: [{name: String, className: String}],
+    descendingFields:  [{name: String, className: String}],
+    textFields: [String],
+    groups: [{name: String, fields: [{name: String, className: String, 
direction: Boolean}]}]
+});
+
+// Define Cache type metadata model.
+exports.CacheTypeMetadata = mongoose.model('CacheTypeMetadata', 
CacheTypeMetadataSchema);
+
+// Define Cache schema.
+var CacheSchema = new Schema({
+    space: {type: ObjectId, ref: 'Space'},
+    name: String,
+    clusters: [{type: ObjectId, ref: 'Cluster'}],
+    metadatas: [{type: ObjectId, ref: 'CacheTypeMetadata'}],
+    cacheMode: {type: String, enum: ['PARTITIONED', 'REPLICATED', 'LOCAL']},
+    atomicityMode: {type: String, enum: ['ATOMIC', 'TRANSACTIONAL']},
+
+    backups: Number,
+    memoryMode: {type: String, enum: ['ONHEAP_TIERED', 'OFFHEAP_TIERED', 
'OFFHEAP_VALUES']},
+    offHeapMaxMemory: Number,
+    startSize: Number,
+    swapEnabled: Boolean,
+
+    evictionPolicy: {
+        kind: {type: String, enum: ['LRU', 'RND', 'FIFO', 'Sorted']},
+        LRU: {
+            batchSize: Number,
+            maxMemorySize: Number,
+            maxSize: Number
+        },
+        RND: {
+            maxSize: Number
+        },
+        FIFO: {
+            batchSize: Number,
+            maxMemorySize: Number,
+            maxSize: Number
+        },
+        SORTED: {
+            batchSize: Number,
+            maxMemorySize: Number,
+            maxSize: Number
+        }
+    },
+
+    rebalanceMode: {type: String, enum: ['SYNC', 'ASYNC', 'NONE']},
+    rebalanceThreadPoolSize: Number,
+    rebalanceBatchSize: Number,
+    rebalanceOrder: Number,
+    rebalanceDelay: Number,
+    rebalanceTimeout: Number,
+    rebalanceThrottle: Number,
+
+    cacheStoreFactory: {
+        kind: {
+            type: String,
+            enum: ['CacheJdbcPojoStoreFactory', 'CacheJdbcBlobStoreFactory', 
'CacheHibernateBlobStoreFactory']
+        },
+        CacheJdbcPojoStoreFactory: {
+            dataSourceBean: String,
+            dialect: {
+                type: String,
+                enum: ['Oracle', 'DB2', 'SQLServer', 'MySQL', 'PosgreSQL', 
'H2']
+            }
+        },
+        CacheJdbcBlobStoreFactory: {
+            user: String,
+            dataSourceBean: String,
+            initSchema: Boolean,
+            createTableQuery: String,
+            loadQuery: String,
+            insertQuery: String,
+            updateQuery: String,
+            deleteQuery: String
+        },
+        CacheHibernateBlobStoreFactory: {
+            hibernateProperties: [String]
+        }
+    },
+    loadPreviousValue: Boolean,
+    readThrough: Boolean,
+    writeThrough: Boolean,
+
+    writeBehindEnabled: Boolean,
+    writeBehindBatchSize: Number,
+    writeBehindFlushSize: Number,
+    writeBehindFlushFrequency: Number,
+    writeBehindFlushThreadCount: Number,
+
+    invalidate: Boolean,
+    defaultLockTimeout: Number,
+
+    sqlEscapeAll: Boolean,
+    sqlOnheapRowCacheSize: Number,
+    longQueryWarningTimeout: Number,
+    indexedTypes: [{keyClass: String, valueClass: String}],
+    sqlFunctionClasses: [String],
+    statisticsEnabled: Boolean,
+    managementEnabled: Boolean,
+    readFromBackup: Boolean,
+    copyOnRead: Boolean,
+    maxConcurrentAsyncOperations: Number,
+    nearCacheEnabled: Boolean,
+    nearConfiguration: {
+        nearStartSize: Number,
+        nearEvictionPolicy: {
+            kind: {type: String, enum: ['LRU', 'RND', 'FIFO', 'Sorted']},
+            LRU: {
+                batchSize: Number,
+                maxMemorySize: Number,
+                maxSize: Number
+            },
+            RND: {
+                maxSize: Number
+            },
+            FIFO: {
+                batchSize: Number,
+                maxMemorySize: Number,
+                maxSize: Number
+            },
+            SORTED: {
+                batchSize: Number,
+                maxMemorySize: Number,
+                maxSize: Number
+            }
+        }
+    }
+});
+
+// Install deep populate plugin.
+CacheSchema.plugin(deepPopulate, {
+    whitelist: ['metadatas']
+});
+
+// Define Cache model.
+exports.Cache = mongoose.model('Cache', CacheSchema);
+
+// Define Cluster schema.
+var ClusterSchema = new Schema({
+    space: {type: ObjectId, ref: 'Space'},
+    name: String,
+    discovery: {
+        localAddress: String,
+        localPort: Number,
+        localPortRange: Number,
+        addressResolver: String,
+        socketTimeout: Number,
+        ackTimeout: Number,
+        maxAckTimeout: Number,
+        networkTimeout: Number,
+        joinTimeout: Number,
+        threadPriority: Number,
+        heartbeatFrequency: Number,
+        maxMissedHeartbeats: Number,
+        maxMissedClientHeartbeats: Number,
+        topHistorySize: Number,
+        listener: String,
+        dataExchange: String,
+        metricsProvider: String,
+        reconnectCount: Number,
+        statisticsPrintFrequency: Number,
+        ipFinderCleanFrequency: Number,
+        authenticator: String,
+        forceServerMode: Boolean,
+        clientReconnectDisabled: Boolean,
+        kind: {type: String, enum: ['Vm', 'Multicast', 'S3', 'Cloud', 
'GoogleStorage', 'Jdbc', 'SharedFs']},
+        Vm: {
+            addresses: [String]
+        },
+        Multicast: {
+            multicastGroup: String,
+            multicastPort: Number,
+            responseWaitTime: Number,
+            addressRequestAttempts: Number,
+            localAddress: String,
+            addresses: [String]
+        },
+        S3: {
+            bucketName: String
+        },
+        Cloud: {
+            credential: String,
+            credentialPath: String,
+            identity: String,
+            provider: String,
+            regions: [String],
+            zones:  [String]
+        },
+        GoogleStorage: {
+            projectName: String,
+            bucketName: String,
+            serviceAccountP12FilePath: String,
+            serviceAccountId: String,
+            addrReqAttempts: String
+        },
+        Jdbc: {
+            initSchema: Boolean
+        },
+        SharedFs: {
+            path: String
+        }
+    },
+    atomicConfiguration: {
+        backups: Number,
+        cacheMode: {type: String, enum: ['LOCAL', 'REPLICATED', 
'PARTITIONED']},
+        atomicSequenceReserveSize: Number
+    },
+    caches: [{type: ObjectId, ref: 'Cache'}],
+    clockSyncSamples: Number,
+    clockSyncFrequency: Number,
+    deploymentMode: {type: String, enum: ['PRIVATE', 'ISOLATED', 'SHARED', 
'CONTINUOUS']},
+    discoveryStartupDelay: Number,
+    igfsThreadPoolSize: Number,
+    includeEventTypes: [{
+        type: String, enum: ['EVTS_CHECKPOINT', 'EVTS_DEPLOYMENT', 
'EVTS_ERROR', 'EVTS_DISCOVERY',
+            'EVTS_JOB_EXECUTION', 'EVTS_TASK_EXECUTION', 'EVTS_CACHE', 
'EVTS_CACHE_REBALANCE', 'EVTS_CACHE_LIFECYCLE',
+            'EVTS_CACHE_QUERY', 'EVTS_SWAPSPACE', 'EVTS_IGFS']
+    }],
+    managementThreadPoolSize: Number,
+    marshaller: {
+        kind: {type: String, enum: ['OptimizedMarshaller', 'JdkMarshaller']},
+        OptimizedMarshaller: {
+            poolSize: Number,
+            requireSerializable: Boolean
+        }
+    },
+    marshalLocalJobs: Boolean,
+    marshallerCacheKeepAliveTime: Number,
+    marshallerCacheThreadPoolSize: Number,
+    metricsExpireTime: Number,
+    metricsHistorySize: Number,
+    metricsLogFrequency: Number,
+    metricsUpdateFrequency: Number,
+    networkTimeout: Number,
+    networkSendRetryDelay: Number,
+    networkSendRetryCount: Number,
+    peerClassLoadingEnabled: Boolean,
+    peerClassLoadingLocalClassPathExclude: [String],
+    peerClassLoadingMissedResourcesCacheSize: Number,
+    peerClassLoadingThreadPoolSize: Number,
+    publicThreadPoolSize: Number,
+    swapSpaceSpi: {
+        kind: {type: String, enum: ['FileSwapSpaceSpi']},
+        FileSwapSpaceSpi: {
+            baseDirectory: String,
+            readStripesNumber: Number,
+            maximumSparsity: Number,
+            maxWriteQueueSize: Number,
+            writeBufferSize: Number
+        }
+    },
+    systemThreadPoolSize: Number,
+    timeServerPortBase: Number,
+    timeServerPortRange: Number,
+    transactionConfiguration: {
+        defaultTxConcurrency: {type: String, enum: ['OPTIMISTIC', 
'PESSIMISTIC']},
+        transactionIsolation: {type: String, enum: ['READ_COMMITTED', 
'REPEATABLE_READ', 'SERIALIZABLE']},
+        defaultTxTimeout: Number,
+        pessimisticTxLogLinger: Number,
+        pessimisticTxLogSize: Number,
+        txSerializableEnabled: Boolean,
+        txManagerLookupClassName: String
+    },
+    sslEnabled: Boolean,
+    sslContextFactory: {
+        keyAlgorithm: String,
+        keyStoreFilePath: String,
+        keyStoreType: String,
+        protocol: String,
+        trustStoreFilePath: String,
+        trustStoreType: String,
+        trustManagers: [String]
+    }
+});
+
+// Install deep populate plugin.
+ClusterSchema.plugin(deepPopulate, {
+    whitelist: [
+        'caches',
+        'caches.metadatas'
+    ]
+});
+
+// Define Cluster model.
+exports.Cluster = mongoose.model('Cluster', ClusterSchema);
+
+// Define Notebook schema.
+var NotebookSchema = new Schema({
+    space: {type: ObjectId, ref: 'Space'},
+    name: String,
+    expandedParagraphs: [Number],
+    paragraphs: [{
+        name: String,
+        query: String,
+        editor: Boolean,
+        result: {type: String, enum: ['none', 'table', 'bar', 'pie', 'line', 
'area']},
+        pageSize: Number,
+        hideSystemColumns: Boolean,
+        cacheName: String,
+        rate: {
+            value: Number,
+            unit: Number
+        }
+    }]
+});
+
+// Define Notebook model.
+exports.Notebook = mongoose.model('Notebook', NotebookSchema);
+
+// Define Database preset schema.
+var DatabasePresetSchema = new Schema({
+    space: {type: ObjectId, ref: 'Space'},
+    jdbcDriverJar: String,
+    jdbcDriverClass: String,
+    jdbcUrl: String,
+    user: String,
+    packageName: String
+});
+
+// Define Database preset model.
+exports.DatabasePreset = mongoose.model('DatabasePreset', 
DatabasePresetSchema);
+
+exports.upsert = function (model, data, cb) {
+    if (data._id) {
+        var id = data._id;
+
+        delete data._id;
+
+        model.findOneAndUpdate({_id: id}, data, cb);
+    }
+    else
+        new model(data).save(cb);
+};
+
+exports.processed = function(err, res) {
+    if (err) {
+        res.status(500).send(err);
+
+        return false;
+    }
+
+    return true;
+};
+
+exports.mongoose = mongoose;

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/helpers/common-utils.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/helpers/common-utils.js 
b/modules/control-center-web/src/main/js/helpers/common-utils.js
new file mode 100644
index 0000000..598cdf8
--- /dev/null
+++ b/modules/control-center-web/src/main/js/helpers/common-utils.js
@@ -0,0 +1,104 @@
+/*
+ *
+ *  * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  * contributor license agreements.  See the NOTICE file distributed with
+ *  * this work for additional information regarding copyright ownership.
+ *  * The ASF licenses this file to You under the Apache License, Version 2.0
+ *  * (the "License"); you may not use this file except in compliance with
+ *  * the License.  You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+// Entry point for common utils.
+$commonUtils = {};
+
+/**
+ * @param v Value to check.
+ * @returns {boolean} 'true' if value defined.
+ */
+$commonUtils.isDefined = function (v) {
+    return !(v === undefined || v === null);
+};
+
+/**
+ * @param v Value to check.
+ * @returns {boolean} 'true' if value defined and not empty string.
+ */
+$commonUtils.isDefinedAndNotEmpty = function (v) {
+    var defined = $commonUtils.isDefined(v);
+
+    if (defined && (typeof(v) == 'string' ||
+        Object.prototype.toString.call(v) === '[object Array]'))
+        defined = v.length > 0;
+
+    return defined;
+};
+
+/**
+ * @param obj Object to check.
+ * @param props Properties names.
+ * @returns {boolean} 'true' if object contains at least one from specified 
properties.
+ */
+$commonUtils.hasProperty = function (obj, props) {
+    for (var propName in props) {
+        if (props.hasOwnProperty(propName)) {
+            if (obj[propName])
+                return true;
+        }
+    }
+
+    return false;
+};
+
+/**
+ * @param obj Object to check.
+ * @param props Array of properties names.
+ * @returns {boolean} 'true' if
+ */
+$commonUtils.hasAtLeastOneProperty = function (obj, props) {
+    if (obj && props) {
+        return _.findIndex(props, function (prop) {
+                return $commonUtils.isDefined(obj[prop]);
+            }) >= 0;
+    }
+
+    return false;
+};
+
+/**
+ * Convert some name to valid java name.
+ *
+ * @param prefix To append to java name.
+ * @param name to convert.
+ * @returns {string} Valid java name.
+ */
+$commonUtils.toJavaName = function (prefix, name) {
+    var javaName = name ? name.replace(/[^A-Za-z_0-9]+/, '_') : 'dflt';
+
+    return prefix + javaName.charAt(0).toLocaleUpperCase() + javaName.slice(1);
+};
+
+$commonUtils.randomString = function (len) {
+    var possible = 
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+    var possibleLen = possible.length;
+
+    var res = '';
+
+    for (var i = 0; i < len; i++)
+        res += possible.charAt(Math.floor(Math.random() * possibleLen));
+
+    return res;
+};
+
+// For server side we should export Java code generation entry point.
+if (typeof window === 'undefined') {
+    module.exports = $commonUtils;
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/helpers/configuration-loader.js
----------------------------------------------------------------------
diff --git 
a/modules/control-center-web/src/main/js/helpers/configuration-loader.js 
b/modules/control-center-web/src/main/js/helpers/configuration-loader.js
new file mode 100644
index 0000000..45616aa
--- /dev/null
+++ b/modules/control-center-web/src/main/js/helpers/configuration-loader.js
@@ -0,0 +1,43 @@
+/*
+ *
+ *  * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  * contributor license agreements.  See the NOTICE file distributed with
+ *  * this work for additional information regarding copyright ownership.
+ *  * The ASF licenses this file to You under the Apache License, Version 2.0
+ *  * (the "License"); you may not use this file except in compliance with
+ *  * the License.  You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+var config = require('nconf');
+
+config.file({'file': 'config/default.json'});
+
+/**
+ * Normalize a port into a number, string, or false.
+ */
+config.normalizePort = function (val) {
+    var port = parseInt(val, 10);
+
+    if (isNaN(port)) {
+        // named pipe
+        return val;
+    }
+
+    if (port >= 0) {
+        // port number
+        return port;
+    }
+
+    return false;
+};
+
+module.exports = config;

Reply via email to