You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by an...@apache.org on 2015/10/13 05:28:15 UTC

[14/18] ignite git commit: IGNITE-843 Web console initial commit.

IGNITE-843 Web console initial commit.


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

Branch: refs/heads/ignite-843-rc1
Commit: bce0deb71f5a70868725ea696644f9b1f96cf2b1
Parents: 81962c4
Author: Andrey <an...@gridgain.com>
Authored: Tue Oct 13 10:06:27 2015 +0700
Committer: Andrey <an...@gridgain.com>
Committed: Tue Oct 13 10:06:27 2015 +0700

----------------------------------------------------------------------
 .../control-center-web/licenses/apache-2.0.txt  |  202 ++
 modules/control-center-web/pom.xml              |   71 +
 .../control-center-web/src/main/js/.gitignore   |    4 +
 .../control-center-web/src/main/js/DEVNOTES.txt |   21 +
 .../src/main/js/agents/agent-manager.js         |  312 +++
 .../src/main/js/agents/agent-server.js          |   98 +
 modules/control-center-web/src/main/js/app.js   |  190 ++
 modules/control-center-web/src/main/js/bin/www  |  126 ++
 .../src/main/js/config/default.json             |   25 +
 .../src/main/js/controllers/admin-controller.js |   81 +
 .../main/js/controllers/caches-controller.js    |  594 ++++++
 .../main/js/controllers/clusters-controller.js  |  560 +++++
 .../src/main/js/controllers/common-module.js    | 2017 ++++++++++++++++++
 .../main/js/controllers/metadata-controller.js  | 1252 +++++++++++
 .../src/main/js/controllers/models/caches.json  | 1027 +++++++++
 .../main/js/controllers/models/clusters.json    | 1333 ++++++++++++
 .../main/js/controllers/models/metadata.json    |  279 +++
 .../src/main/js/controllers/models/summary.json |  172 ++
 .../main/js/controllers/profile-controller.js   |   94 +
 .../src/main/js/controllers/sql-controller.js   | 1097 ++++++++++
 .../main/js/controllers/summary-controller.js   |  233 ++
 modules/control-center-web/src/main/js/db.js    |  431 ++++
 .../src/main/js/helpers/common-utils.js         |  104 +
 .../src/main/js/helpers/configuration-loader.js |   43 +
 .../src/main/js/helpers/data-structures.js      |  113 +
 .../src/main/js/keys/test.crt                   |   13 +
 .../src/main/js/keys/test.key                   |   18 +
 .../control-center-web/src/main/js/package.json |   53 +
 .../public/stylesheets/_bootstrap-custom.scss   |   67 +
 .../stylesheets/_bootstrap-variables.scss       |  890 ++++++++
 .../src/main/js/public/stylesheets/style.scss   | 1838 ++++++++++++++++
 .../src/main/js/routes/admin.js                 |  127 ++
 .../src/main/js/routes/agent.js                 |  261 +++
 .../src/main/js/routes/caches.js                |  171 ++
 .../src/main/js/routes/clusters.js              |  145 ++
 .../js/routes/generator/generator-common.js     |  353 +++
 .../js/routes/generator/generator-docker.js     |   60 +
 .../main/js/routes/generator/generator-java.js  | 1581 ++++++++++++++
 .../js/routes/generator/generator-properties.js |  112 +
 .../main/js/routes/generator/generator-xml.js   | 1202 +++++++++++
 .../src/main/js/routes/metadata.js              |  192 ++
 .../src/main/js/routes/notebooks.js             |  157 ++
 .../src/main/js/routes/presets.js               |   70 +
 .../src/main/js/routes/profile.js               |  105 +
 .../src/main/js/routes/public.js                |  266 +++
 .../src/main/js/routes/sql.js                   |   39 +
 .../src/main/js/routes/summary.js               |  104 +
 .../src/main/js/views/configuration/caches.jade |   46 +
 .../main/js/views/configuration/clusters.jade   |   46 +
 .../js/views/configuration/metadata-load.jade   |   89 +
 .../main/js/views/configuration/metadata.jade   |   65 +
 .../main/js/views/configuration/sidebar.jade    |   48 +
 .../js/views/configuration/summary-tabs.jade    |   24 +
 .../main/js/views/configuration/summary.jade    |  124 ++
 .../src/main/js/views/error.jade                |   22 +
 .../src/main/js/views/includes/controls.jade    |  515 +++++
 .../src/main/js/views/includes/footer.jade      |   22 +
 .../src/main/js/views/includes/header.jade      |   40 +
 .../src/main/js/views/index.jade                |  141 ++
 .../src/main/js/views/reset.jade                |   38 +
 .../src/main/js/views/settings/admin.jade       |   57 +
 .../src/main/js/views/settings/profile.jade     |   68 +
 .../src/main/js/views/sql/cache-metadata.jade   |   26 +
 .../src/main/js/views/sql/chart-settings.jade   |   38 +
 .../src/main/js/views/sql/notebook-new.jade     |   31 +
 .../src/main/js/views/sql/paragraph-rate.jade   |   31 +
 .../src/main/js/views/sql/sql.jade              |  173 ++
 .../main/js/views/templates/agent-download.jade |   49 +
 .../main/js/views/templates/batch-confirm.jade  |   32 +
 .../src/main/js/views/templates/clone.jade      |   32 +
 .../src/main/js/views/templates/confirm.jade    |   27 +
 .../src/main/js/views/templates/layout.jade     |   82 +
 .../src/main/js/views/templates/message.jade    |   26 +
 .../src/main/js/views/templates/select.jade     |   26 +
 .../js/views/templates/validation-error.jade    |   25 +
 .../src/test/js/routes/agent.js                 |   96 +
 76 files changed, 20342 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/licenses/apache-2.0.txt
----------------------------------------------------------------------
diff --git a/modules/control-center-web/licenses/apache-2.0.txt b/modules/control-center-web/licenses/apache-2.0.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/modules/control-center-web/licenses/apache-2.0.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/pom.xml
----------------------------------------------------------------------
diff --git a/modules/control-center-web/pom.xml b/modules/control-center-web/pom.xml
new file mode 100644
index 0000000..fcd9b91
--- /dev/null
+++ b/modules/control-center-web/pom.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  ~ /*
+  ~  * Licensed to the Apache Software Foundation (ASF) under one or more
+  ~  * contributor license agreements.  See the NOTICE file distributed with
+  ~  * this work for additional information regarding copyright ownership.
+  ~  * The ASF licenses this file to You under the Apache License, Version 2.0
+  ~  * (the "License"); you may not use this file except in compliance with
+  ~  * the License.  You may obtain a copy of the License at
+  ~  *
+  ~  *      http://www.apache.org/licenses/LICENSE-2.0
+  ~  *
+  ~  * Unless required by applicable law or agreed to in writing, software
+  ~  * distributed under the License is distributed on an "AS IS" BASIS,
+  ~  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~  * See the License for the specific language governing permissions and
+  ~  * limitations under the License.
+  ~  */
+  -->
+
+<!--
+    POM file.
+-->
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.ignite</groupId>
+        <artifactId>ignite-parent</artifactId>
+        <version>1</version>
+        <relativePath>../../parent</relativePath>
+    </parent>
+
+    <artifactId>ignite-control-center-web</artifactId>
+    <version>1.5.0-SNAPSHOT</version>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>com.github.eirslett</groupId>
+                <artifactId>frontend-maven-plugin</artifactId>
+                <version>0.0.23</version>
+
+                <configuration>
+                    <workingDirectory>src/main/js</workingDirectory>
+                </configuration>
+
+                <executions>
+                    <execution>
+                        <id>install node and npm</id>
+                        <goals>
+                            <goal>install-node-and-npm</goal>
+                        </goals>
+                        <configuration>
+                            <nodeVersion>v0.12.7</nodeVersion>
+                            <npmVersion>2.13.2</npmVersion>
+                        </configuration>
+                    </execution>
+
+                    <execution>
+                        <id>npm install</id>
+                        <goals>
+                            <goal>npm</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/.gitignore
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/.gitignore b/modules/control-center-web/src/main/js/.gitignore
new file mode 100644
index 0000000..96db808
--- /dev/null
+++ b/modules/control-center-web/src/main/js/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+*.idea
+*.log
+*.css

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/DEVNOTES.txt
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/DEVNOTES.txt b/modules/control-center-web/src/main/js/DEVNOTES.txt
new file mode 100644
index 0000000..4859cf1
--- /dev/null
+++ b/modules/control-center-web/src/main/js/DEVNOTES.txt
@@ -0,0 +1,21 @@
+Ignite Web Console Instructions
+======================================
+
+How to deploy:
+
+1. Install locally NodeJS using installer from site https://nodejs.org for your OS.
+2. Install locally MongoDB follow instructions from site http://docs.mongodb.org/manual/installation
+3. Checkout ignite-843 branch.
+4. Change directory '$IGNITE_HOME/modules/control-center-web/src/main/js'.
+5. Run "npm install" in terminal for download all dependencies.
+
+Steps 1 - 5 should be executed once.
+
+How to run:
+
+1. Run MongoDB.
+ 1.1 In terminal change dir to $MONGO_INSTALL_DIR/server/3.0/bin.
+ 1.2 Run "mongod".
+2. In new terminal change directory '$IGNITE_HOME/modules/control-center-web/src/main/js'.
+3. Start application by executing "npm start".
+4. In browser open: http://localhost:3000

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/agents/agent-manager.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/agents/agent-manager.js b/modules/control-center-web/src/main/js/agents/agent-manager.js
new file mode 100644
index 0000000..582cb11
--- /dev/null
+++ b/modules/control-center-web/src/main/js/agents/agent-manager.js
@@ -0,0 +1,312 @@
+/*
+ *
+ *  * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  * contributor license agreements.  See the NOTICE file distributed with
+ *  * this work for additional information regarding copyright ownership.
+ *  * The ASF licenses this file to You under the Apache License, Version 2.0
+ *  * (the "License"); you may not use this file except in compliance with
+ *  * the License.  You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT 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 WebSocketServer = require('ws').Server;
+
+var apacheIgnite = require('apache-ignite');
+
+var db = require('../db');
+
+var AgentServer = require('./agent-server').AgentServer;
+
+/**
+ * @constructor
+ */
+function AgentManager(srv) {
+    this._clients = {};
+
+    this._server = srv;
+
+    this._wss = new WebSocketServer({ server: this._server });
+
+    var self = this;
+
+    this._wss.on('connection', function(ws) {
+        new Client(ws, self);
+    });
+}
+
+/**
+ * @param userId
+ * @param {Client} client
+ */
+AgentManager.prototype._removeClient = function(userId, client) {
+    var connections = this._clients[userId];
+
+    if (connections) {
+        removeFromArray(connections, client);
+
+        if (connections.length == 0)
+            delete this._clients[userId];
+    }
+};
+
+/**
+ * @param userId
+ * @param {Client} client
+ */
+AgentManager.prototype._addClient = function(userId, client) {
+    var existingConnections = this._clients[userId];
+
+    if (!existingConnections) {
+        existingConnections = [];
+
+        this._clients[userId] = existingConnections;
+    }
+
+    existingConnections.push(client);
+};
+
+/**
+ * @param userId
+ * @return {Client}
+ */
+AgentManager.prototype.findClient = function(userId) {
+    var clientsList = this._clients[userId];
+
+    if (!clientsList || clientsList.length == 0)
+        return null;
+
+    return clientsList[0];
+};
+
+/**
+ * @constructor
+ * @param {AgentManager} manager
+ * @param {WebSocket} ws
+ */
+function Client(ws, manager) {
+    var self = this;
+
+    this._manager = manager;
+    this._ws = ws;
+
+    ws.on('close', function() {
+        if (self._user) {
+            self._manager._removeClient(self._user._id, self);
+        }
+    });
+
+    ws.on('message', function (msgStr) {
+        var msg = JSON.parse(msgStr);
+
+        self['_rmt' + msg.type](msg);
+    });
+
+    this._reqCounter = 0;
+
+    this._cbMap = {};
+}
+
+/**
+ * @param {String} path
+ * @param {Object} params
+ * @param {String} [method]
+ * @param {Object} [headers]
+ * @param {String} [body]
+ * @param {Function} [cb] Callback. Take 3 arguments: {String} error, {number} httpCode, {string} response.
+ */
+Client.prototype.executeRest = function(path, params, method, headers, body, cb) {
+    if (typeof(params) != 'object')
+        throw '"params" argument must be an object';
+
+    if (typeof(cb) != 'function')
+        throw 'callback must be a function';
+
+    if (body && typeof(body) != 'string')
+        throw 'body must be a string';
+
+    if (headers && typeof(headers) != 'object')
+        throw 'headers must be an object';
+
+    if (!method)
+        method = 'GET';
+    else
+        method = method.toUpperCase();
+
+    if (method != 'GET' && method != 'POST')
+        throw 'Unknown HTTP method: ' + method;
+
+    var newArgs = argsToArray(arguments);
+
+    newArgs[5] = function(ex, res) {
+        if (ex)
+            cb(ex.message);
+        else
+            cb(null, res.code, res.message)
+    };
+
+    this._invokeRmtMethod('executeRest', newArgs);
+};
+
+/**
+ * @param {string} error
+ */
+Client.prototype.authResult = function(error) {
+    this._invokeRmtMethod('authResult', arguments)
+};
+
+/**
+ * @param {String} jdbcDriverJarPath
+ * @param {String} jdbcDriverClass
+ * @param {String} jdbcUrl
+ * @param {Object} jdbcInfo
+ * @param {Function} cb Callback. Take two arguments: {Object} exception, {Object} result.
+ * @return {Array} List of tables (see org.apache.ignite.schema.parser.DbTable java class)
+ */
+Client.prototype.metadataSchemas = function(jdbcDriverJarPath, jdbcDriverClass, jdbcUrl, jdbcInfo, cb) {
+    this._invokeRmtMethod('schemas', arguments)
+};
+
+/**
+ * @param {String} jdbcDriverJarPath
+ * @param {String} jdbcDriverClass
+ * @param {String} jdbcUrl
+ * @param {Object} jdbcInfo
+ * @param {Array} schemas
+ * @param {Boolean} tablesOnly
+ * @param {Function} cb Callback. Take two arguments: {Object} exception, {Object} result.
+ * @return {Array} List of tables (see org.apache.ignite.schema.parser.DbTable java class)
+ */
+Client.prototype.metadataTables = function(jdbcDriverJarPath, jdbcDriverClass, jdbcUrl, jdbcInfo, schemas, tablesOnly, cb) {
+    this._invokeRmtMethod('metadata', arguments)
+};
+
+/**
+ * @param {Function} cb Callback. Take two arguments: {Object} exception, {Object} result.
+ * @return {Array} List of jars from driver folder.
+ */
+Client.prototype.availableDrivers = function(cb) {
+    this._invokeRmtMethod('availableDrivers', arguments)
+};
+
+Client.prototype._invokeRmtMethod = function(methodName, args) {
+    var cb = null;
+
+    var m = argsToArray(args);
+
+    if (m.length > 0 && typeof m[m.length - 1] == 'function')
+        cb = m.pop();
+
+    if (this._ws.readyState != 1) {
+        if (cb)
+            cb({type: 'org.apache.ignite.agent.AgentException', message: 'Connection is closed'});
+
+        return
+    }
+
+    var msg = {
+        mtdName: methodName,
+        args: m
+    };
+
+    if (cb) {
+        var reqId = this._reqCounter++;
+
+        this._cbMap[reqId] = cb;
+
+        msg.reqId = reqId;
+    }
+
+    this._ws.send(JSON.stringify(msg))
+};
+
+Client.prototype._rmtAuthMessage = function(msg) {
+    var self = this;
+
+    db.Account.findOne({ token: msg.token }, function (err, account) {
+        if (err) {
+            self.authResult('Failed to authorize user');
+            // TODO IGNITE-1379 send error to web master.
+        }
+        else if (!account)
+            self.authResult('Invalid token, user not found');
+        else {
+            self.authResult(null);
+
+            self._user = account;
+
+            self._manager._addClient(account._id, self);
+
+            self._ignite = new apacheIgnite.Ignite(new AgentServer(self));
+        }
+    });
+};
+
+Client.prototype._rmtCallRes = function(msg) {
+    var cb = this._cbMap[msg.reqId];
+
+    if (!cb) return;
+
+    delete this._cbMap[msg.reqId];
+
+    if (msg.ex)
+        cb(msg.ex);
+    else
+        cb(null, msg.res);
+};
+
+/**
+ * @return {Ignite}
+ */
+Client.prototype.ignite = function() {
+    return this._ignite;
+};
+
+function removeFromArray(arr, val) {
+    var idx;
+
+    while ((idx = arr.indexOf(val)) !== -1) {
+        arr.splice(idx, 1);
+    }
+}
+
+/**
+ * @param args
+ * @returns {Array}
+ */
+function argsToArray(args) {
+    var res = [];
+
+    for (var i = 0; i < args.length; i++)
+        res.push(args[i])
+
+    return res;
+}
+
+exports.AgentManager = AgentManager;
+
+/**
+ * @type {AgentManager}
+ */
+var manager = null;
+
+exports.createManager = function(srv) {
+    if (manager)
+        throw 'Agent manager already cleared!';
+
+    manager = new AgentManager(srv);
+};
+
+/**
+ * @return {AgentManager}
+ */
+exports.getAgentManager = function() {
+    return manager;
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/agents/agent-server.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/agents/agent-server.js b/modules/control-center-web/src/main/js/agents/agent-server.js
new file mode 100644
index 0000000..bd7efbf
--- /dev/null
+++ b/modules/control-center-web/src/main/js/agents/agent-server.js
@@ -0,0 +1,98 @@
+/*
+ *
+ *  * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  * contributor license agreements.  See the NOTICE file distributed with
+ *  * this work for additional information regarding copyright ownership.
+ *  * The ASF licenses this file to You under the Apache License, Version 2.0
+ *  * (the "License"); you may not use this file except in compliance with
+ *  * the License.  You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+var _ = require('lodash');
+
+/**
+ * Creates an instance of server for Ignite
+ *
+ * @constructor
+ * @this {AgentServer}
+ * @param {Client} client connected client
+ */
+function AgentServer(client) {
+    this._client = client;
+}
+
+/**
+ * Run http request
+ *
+ * @this {AgentServer}
+ * @param {Command} cmd Command
+ * @param {callback} callback on finish
+ */
+AgentServer.prototype.runCommand = function(cmd, callback) {
+    var params = {cmd: cmd.name()};
+
+    _.forEach(cmd._params, function (p) {
+        params[p.key] = p.value;
+    });
+
+    var body = undefined;
+
+    var headers = undefined;
+
+    var method = 'GET';
+
+    if (cmd._isPost()) {
+        body = cmd.postData();
+
+        method = 'POST';
+
+        headers = {'JSONObject': 'application/json'};
+    }
+
+    this._client.executeRest("ignite", params, method, headers, body, function(error, code, message) {
+        if (error) {
+            callback(error);
+            return
+        }
+
+        if (code !== 200) {
+            if (code === 401) {
+                callback.call(null, "Authentication failed. Status code 401.");
+            }
+            else {
+                callback.call(null, "Request failed. Status code " + code);
+            }
+
+            return;
+        }
+
+        var igniteResponse;
+
+        try {
+            igniteResponse = JSON.parse(message);
+        }
+        catch (e) {
+            callback.call(null, e, null);
+
+            return;
+        }
+
+        if (igniteResponse.successStatus) {
+            callback.call(null, igniteResponse.error, null)
+        }
+        else {
+            callback.call(null, null, igniteResponse.response);
+        }
+    });
+};
+
+exports.AgentServer = AgentServer;

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/app.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/app.js b/modules/control-center-web/src/main/js/app.js
new file mode 100644
index 0000000..69f6663
--- /dev/null
+++ b/modules/control-center-web/src/main/js/app.js
@@ -0,0 +1,190 @@
+/*
+ *
+ *  * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  * contributor license agreements.  See the NOTICE file distributed with
+ *  * this work for additional information regarding copyright ownership.
+ *  * The ASF licenses this file to You under the Apache License, Version 2.0
+ *  * (the "License"); you may not use this file except in compliance with
+ *  * the License.  You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+var express = require('express');
+var compress = require('compression');
+var path = require('path');
+var favicon = require('serve-favicon');
+var logger = require('morgan');
+var cookieParser = require('cookie-parser');
+var bodyParser = require('body-parser');
+var session = require('express-session');
+var mongoStore = require('connect-mongo')(session);
+var forceSSL = require('express-force-ssl');
+var config = require('./helpers/configuration-loader.js');
+
+var publicRoutes = require('./routes/public');
+var notebooksRoutes = require('./routes/notebooks');
+var clustersRouter = require('./routes/clusters');
+var cachesRouter = require('./routes/caches');
+var metadataRouter = require('./routes/metadata');
+var presetsRouter = require('./routes/presets');
+var summary = require('./routes/summary');
+var adminRouter = require('./routes/admin');
+var profileRouter = require('./routes/profile');
+var sqlRouter = require('./routes/sql');
+var agentRouter = require('./routes/agent');
+
+var passport = require('passport');
+
+var db = require('./db');
+
+var app = express();
+
+app.use(compress());
+
+app.use(bodyParser.json({limit: '50mb'}));
+app.use(bodyParser.urlencoded({limit: '50mb', extended: true}));
+
+// Views engine setup.
+app.set('views', path.join(__dirname, 'views'));
+app.set('view engine', 'jade');
+
+// Site favicon.
+app.use(favicon(__dirname + '/public/favicon.ico'));
+
+app.use(logger('dev'));
+
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({extended: false}));
+
+app.use(require('node-sass-middleware')({
+    /* Options */
+    src: path.join(__dirname, 'public'),
+    dest: path.join(__dirname, 'public'),
+    debug: true,
+    outputStyle: 'nested'
+}));
+
+app.use(express.static(path.join(__dirname, 'public')));
+app.use(express.static(path.join(__dirname, 'controllers')));
+app.use(express.static(path.join(__dirname, 'helpers')));
+app.use(express.static(path.join(__dirname, 'routes/generator')));
+
+app.use(cookieParser('keyboard cat'));
+
+app.use(session({
+    secret: 'keyboard cat',
+    resave: false,
+    saveUninitialized: true,
+    store: new mongoStore({
+        mongooseConnection: db.mongoose.connection
+    })
+}));
+
+app.use(passport.initialize());
+app.use(passport.session());
+
+passport.serializeUser(db.Account.serializeUser());
+passport.deserializeUser(db.Account.deserializeUser());
+
+passport.use(db.Account.createStrategy());
+
+if (config.get('server:ssl')) {
+    var httpsPort = config.normalizePort(config.get('server:https-port') || 443);
+
+    app.set('forceSSLOptions', {
+        enable301Redirects: true,
+        trustXFPHeader: true,
+        httpsPort: httpsPort
+    });
+
+    app.use(forceSSL);
+}
+
+var mustAuthenticated = function (req, res, next) {
+    req.isAuthenticated() ? next() : res.redirect('/');
+};
+
+var adminOnly = function(req, res, next) {
+    req.isAuthenticated() && req.user.admin ? next() : res.sendStatus(403);
+};
+
+app.all('/configuration/*', mustAuthenticated);
+
+app.all('*', function(req, res, next) {
+    var becomeUsed = req.session.viewedUser && req.user.admin;
+
+    if (req.url.lastIndexOf('/reset', 0) === 0) {
+        res.locals.user = null;
+        res.locals.becomeUsed = false;
+    }
+    else {
+        res.locals.user = becomeUsed ? req.session.viewedUser : req.user;
+        res.locals.becomeUsed = becomeUsed;
+    }
+
+    req.currentUserId = function() {
+        if (!req.user)
+            return null;
+
+        if (req.session.viewedUser && req.user.admin)
+            return req.session.viewedUser._id;
+
+        return req.user._id;
+    };
+
+    next();
+});
+
+app.use('/', publicRoutes);
+app.use('/admin', mustAuthenticated, adminOnly, adminRouter);
+app.use('/profile', mustAuthenticated, profileRouter);
+
+app.use('/configuration/clusters', clustersRouter);
+app.use('/configuration/caches', cachesRouter);
+app.use('/configuration/metadata', metadataRouter);
+app.use('/configuration/presets', presetsRouter);
+app.use('/configuration/summary', summary);
+
+app.use('/notebooks', mustAuthenticated, notebooksRoutes);
+app.use('/sql', mustAuthenticated, sqlRouter);
+
+app.use('/agent', mustAuthenticated, agentRouter);
+
+// Catch 404 and forward to error handler.
+app.use(function (req, res, next) {
+    var err = new Error('Not Found: ' + req.originalUrl);
+    err.status = 404;
+    next(err);
+});
+
+// Error handlers.
+
+// Development error handler: will print stacktrace.
+if (app.get('env') === 'development') {
+    app.use(function (err, req, res) {
+        res.status(err.status || 500);
+        res.render('error', {
+            message: err.message,
+            error: err
+        });
+    });
+}
+
+// Production error handler: no stacktraces leaked to user.
+app.use(function (err, req, res) {
+    res.status(err.status || 500);
+    res.render('error', {
+        message: err.message,
+        error: {}
+    });
+});
+
+module.exports = app;

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/bin/www
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/bin/www b/modules/control-center-web/src/main/js/bin/www
new file mode 100644
index 0000000..69e73e3
--- /dev/null
+++ b/modules/control-center-web/src/main/js/bin/www
@@ -0,0 +1,126 @@
+/*
+ *
+ *  * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  * contributor license agreements.  See the NOTICE file distributed with
+ *  * this work for additional information regarding copyright ownership.
+ *  * The ASF licenses this file to You under the Apache License, Version 2.0
+ *  * (the "License"); you may not use this file except in compliance with
+ *  * the License.  You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+var http = require('http');
+var https = require('https');
+var config = require('../helpers/configuration-loader.js');
+var app = require('../app');
+var agentManager = require('../agents/agent-manager');
+
+var fs = require('fs');
+
+var debug = require('debug')('ignite-web-console:server');
+
+/**
+ * Get port from environment and store in Express.
+ */
+var port = config.normalizePort(config.get('server:port') || process.env.PORT || 80);
+
+// Create HTTP server.
+var server = http.createServer(app);
+
+app.set('port', port);
+
+/**
+ * Listen on provided port, on all network interfaces.
+ */
+server.listen(port);
+server.on('error', onError);
+server.on('listening', onListening);
+
+if (config.get('server:ssl')) {
+    httpsServer = https.createServer({
+        key: fs.readFileSync(config.get('server:key')),
+        cert: fs.readFileSync(config.get('server:cert')),
+        passphrase: config.get('server:keyPassphrase')
+    }, app);
+
+    var httpsPort = config.normalizePort(config.get('server:https-port') || 443);
+
+    /**
+     * Listen on provided port, on all network interfaces.
+     */
+    httpsServer.listen(httpsPort);
+    httpsServer.on('error', onError);
+    httpsServer.on('listening', onListening);
+}
+
+/**
+ * Start agent server.
+ */
+var agentServer;
+
+if (config.get('agent-server:ssl')) {
+    agentServer = https.createServer({
+    key: fs.readFileSync(config.get('agent-server:key')),
+    cert: fs.readFileSync(config.get('agent-server:cert')),
+    passphrase: config.get('agent-server:keyPassphrase')
+  });
+}
+else {
+  agentServer = http.createServer();
+}
+
+agentServer.listen(config.get('agent-server:port'));
+
+agentManager.createManager(agentServer);
+
+/**
+ * Event listener for HTTP server "error" event.
+ */
+function onError(error) {
+  if (error.syscall !== 'listen') {
+    throw error;
+  }
+
+  var bind = typeof port === 'string'
+    ? 'Pipe ' + port
+    : 'Port ' + port;
+
+  // handle specific listen errors with friendly messages
+  switch (error.code) {
+    case 'EACCES':
+      console.error(bind + ' requires elevated privileges');
+      process.exit(1);
+      break;
+    case 'EADDRINUSE':
+      console.error(bind + ' is already in use');
+      process.exit(1);
+      break;
+    default:
+      throw error;
+  }
+}
+
+/**
+ * Event listener for HTTP server "listening" event.
+ */
+function onListening() {
+  var addr = server.address();
+  var bind = typeof addr === 'string'
+    ? 'pipe ' + addr
+    : 'port ' + addr.port;
+
+  debug('Listening on ' + bind);
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/config/default.json
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/config/default.json b/modules/control-center-web/src/main/js/config/default.json
new file mode 100644
index 0000000..bf1e88b
--- /dev/null
+++ b/modules/control-center-web/src/main/js/config/default.json
@@ -0,0 +1,25 @@
+{
+    "server": {
+        "port": 3000,
+        "https-port": 8443,
+        "ssl": false,
+        "key": "keys/test.key",
+        "cert": "keys/test.crt",
+        "keyPassphrase": "password"
+    },
+    "mongoDB": {
+        "url": "mongodb://localhost/web-control-center"
+    },
+    "agent-server": {
+        "port": 3001,
+        "ssl": true,
+        "key": "keys/test.key",
+        "cert": "keys/test.crt",
+        "keyPassphrase": "password"
+    },
+    "smtp": {
+        "service": "",
+        "username": "",
+        "password": ""
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/controllers/admin-controller.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/controllers/admin-controller.js b/modules/control-center-web/src/main/js/controllers/admin-controller.js
new file mode 100644
index 0000000..0b57da5
--- /dev/null
+++ b/modules/control-center-web/src/main/js/controllers/admin-controller.js
@@ -0,0 +1,81 @@
+/*
+ *
+ *  * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  * contributor license agreements.  See the NOTICE file distributed with
+ *  * this work for additional information regarding copyright ownership.
+ *  * The ASF licenses this file to You under the Apache License, Version 2.0
+ *  * (the "License"); you may not use this file except in compliance with
+ *  * the License.  You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+
+// Controller for Admin screen.
+consoleModule.controller('adminController',
+    ['$scope', '$window', '$http', '$common', '$confirm',
+    function ($scope, $window, $http, $common, $confirm) {
+    $scope.users = null;
+
+    function reload() {
+        $http.post('admin/list')
+            .success(function (data) {
+                $scope.users = data;
+            })
+            .error(function (errMsg) {
+                $common.showError($common.errorMessage(errMsg));
+            });
+    }
+
+    reload();
+
+    $scope.becomeUser = function (user) {
+        $window.location = '/admin/become?viewedUserId=' + user._id;
+    };
+
+    $scope.removeUser = function (user) {
+        $confirm.confirm('Are you sure you want to remove user: "' + user.username + '"?')
+            .then(function () {
+                $http.post('admin/remove', {userId: user._id}).success(
+                    function () {
+                        var i = _.findIndex($scope.users, function (u) {
+                            return u._id == user._id;
+                        });
+
+                        if (i >= 0)
+                            $scope.users.splice(i, 1);
+
+                        $common.showInfo('User has been removed: "' + user.username + '"');
+                    }).error(function (errMsg, status) {
+                        if (status == 503)
+                            $common.showInfo(errMsg);
+                        else
+                            $common.showError('Failed to remove user: "' + $common.errorMessage(errMsg) + '"');
+                    });
+            });
+    };
+
+    $scope.toggleAdmin = function (user) {
+        if (user.adminChanging)
+            return;
+
+        user.adminChanging = true;
+
+        $http.post('admin/save', {userId: user._id, adminFlag: user.admin}).success(
+            function () {
+                $common.showInfo('Admin right was successfully toggled for user: "' + user.username + '"');
+
+                user.adminChanging = false;
+            }).error(function (errMsg) {
+                $common.showError('Failed to toggle admin right for user: "' + $common.errorMessage(errMsg) + '"');
+
+                user.adminChanging = false;
+            });
+    }
+}]);

http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/controllers/caches-controller.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/controllers/caches-controller.js b/modules/control-center-web/src/main/js/controllers/caches-controller.js
new file mode 100644
index 0000000..28cedf0
--- /dev/null
+++ b/modules/control-center-web/src/main/js/controllers/caches-controller.js
@@ -0,0 +1,594 @@
+/*
+ *
+ *  * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  * contributor license agreements.  See the NOTICE file distributed with
+ *  * this work for additional information regarding copyright ownership.
+ *  * The ASF licenses this file to You under the Apache License, Version 2.0
+ *  * (the "License"); you may not use this file except in compliance with
+ *  * the License.  You may obtain a copy of the License at
+ *  *
+ *  *      http://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT 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 Caches screen.
+consoleModule.controller('cachesController', [
+    '$scope', '$controller', '$filter', '$http', '$timeout', '$common', '$focus', '$confirm', '$message', '$clone', '$table', '$preview', '$loading', '$unsavedChangesGuard',
+    function ($scope, $controller, $filter, $http, $timeout, $common, $focus, $confirm, $message, $clone, $table, $preview, $loading, $unsavedChangesGuard) {
+            $unsavedChangesGuard.install($scope);
+
+            // Initialize the super class and extend it.
+            angular.extend(this, $controller('save-remove', {$scope: $scope}));
+
+            $scope.ui = $common.formUI();
+
+            $scope.showMoreInfo = $message.message;
+
+            $scope.joinTip = $common.joinTip;
+            $scope.getModel = $common.getModel;
+            $scope.javaBuildInClasses = $common.javaBuildInClasses;
+            $scope.compactJavaName = $common.compactJavaName;
+            $scope.saveBtnTipText = $common.saveBtnTipText;
+
+            $scope.tableReset = $table.tableReset;
+            $scope.tableNewItem = $table.tableNewItem;
+            $scope.tableNewItemActive = $table.tableNewItemActive;
+            $scope.tableEditing = $table.tableEditing;
+            $scope.tableStartEdit = $table.tableStartEdit;
+            $scope.tableRemove = function (item, field, index) {
+                $table.tableRemove(item, field, index);
+            };
+
+            $scope.tableSimpleSave = $table.tableSimpleSave;
+            $scope.tableSimpleSaveVisible = $table.tableSimpleSaveVisible;
+            $scope.tableSimpleUp = $table.tableSimpleUp;
+            $scope.tableSimpleDown = $table.tableSimpleDown;
+            $scope.tableSimpleDownVisible = $table.tableSimpleDownVisible;
+
+            $scope.tablePairSave = $table.tablePairSave;
+            $scope.tablePairSaveVisible = $table.tablePairSaveVisible;
+
+            var previews = [];
+
+            $scope.previewInit = function (preview) {
+                previews.push(preview);
+
+                $preview.previewInit(preview);
+            };
+
+            $scope.previewChanged = $preview.previewChanged;
+
+            $scope.hidePopover = $common.hidePopover;
+
+            var showPopoverMessage = $common.showPopoverMessage;
+
+            $scope.atomicities = $common.mkOptions(['ATOMIC', 'TRANSACTIONAL']);
+
+            $scope.cacheModes = $common.mkOptions(['PARTITIONED', 'REPLICATED', 'LOCAL']);
+
+            $scope.atomicWriteOrderModes = $common.mkOptions(['CLOCK', 'PRIMARY']);
+
+            $scope.memoryModes = $common.mkOptions(['ONHEAP_TIERED', 'OFFHEAP_TIERED', 'OFFHEAP_VALUES']);
+
+            $scope.evictionPolicies = [
+                {value: 'LRU', label: 'LRU'},
+                {value: 'RND', label: 'Random'},
+                {value: 'FIFO', label: 'FIFO'},
+                {value: 'SORTED', label: 'Sorted'},
+                {value: undefined, label: 'Not set'}
+            ];
+
+            $scope.rebalanceModes = $common.mkOptions(['SYNC', 'ASYNC', 'NONE']);
+
+            $scope.cacheStoreFactories = [
+                {value: 'CacheJdbcPojoStoreFactory', label: 'JDBC POJO store factory'},
+                {value: 'CacheJdbcBlobStoreFactory', label: 'JDBC BLOB store factory'},
+                {value: 'CacheHibernateBlobStoreFactory', label: 'Hibernate BLOB store factory'},
+                {value: undefined, label: 'Not set'}
+            ];
+
+            $scope.cacheStoreJdbcDialects = [
+                {value: 'Oracle', label: 'Oracle'},
+                {value: 'DB2', label: 'IBM DB2'},
+                {value: 'SQLServer', label: 'Microsoft SQL Server'},
+                {value: 'MySQL', label: 'My SQL'},
+                {value: 'PostgreSQL', label: 'Postgre SQL'},
+                {value: 'H2', label: 'H2 database'}
+            ];
+
+            $scope.toggleExpanded = function () {
+                $scope.ui.expanded = !$scope.ui.expanded;
+
+                $common.hidePopover();
+            };
+
+            $scope.panels = {activePanels: [0]};
+
+            $scope.general = [];
+            $scope.advanced = [];
+            $scope.caches = [];
+            $scope.metadatas = [];
+
+            $scope.preview = {
+                general: {xml: '', java: '', allDefaults: true},
+                memory: {xml: '', java: '', allDefaults: true},
+                query: {xml: '', java: '', allDefaults: true},
+                store: {xml: '', java: '', allDefaults: true},
+                concurrency: {xml: '', java: '', allDefaults: true},
+                rebalance: {xml: '', java: '', allDefaults: true},
+                serverNearCache: {xml: '', java: '', allDefaults: true},
+                statistics: {xml: '', java: '', allDefaults: true}
+            };
+
+            $scope.required = function (field) {
+                var model = $common.isDefined(field.path) ? field.path + '.' + field.model : field.model;
+
+                var backupItem = $scope.backupItem;
+
+                var memoryMode = backupItem.memoryMode;
+
+                var onHeapTired = memoryMode == 'ONHEAP_TIERED';
+                var offHeapTired = memoryMode == 'OFFHEAP_TIERED';
+
+                var offHeapMaxMemory = backupItem.offHeapMaxMemory;
+
+                if (model == 'offHeapMaxMemory' && offHeapTired)
+                    return true;
+
+                if (model == 'evictionPolicy.kind' && onHeapTired)
+                    return backupItem.swapEnabled || ($common.isDefined(offHeapMaxMemory) && offHeapMaxMemory >= 0);
+
+                return false;
+            };
+
+            $scope.tableSimpleValid = function (item, field, fx, index) {
+                var model;
+
+                switch (field.model) {
+                    case 'hibernateProperties':
+                        if (fx.indexOf('=') < 0)
+                            return showPopoverMessage(null, null, $table.tableFieldId(index, 'HibProp'), 'Property should be present in format key=value!');
+
+                        model = item.cacheStoreFactory.CacheHibernateBlobStoreFactory[field.model];
+
+                        var key = fx.split('=')[0];
+
+                        var exist = false;
+
+                        if ($common.isDefined(model)) {
+                            model.forEach(function (val) {
+                                if (val.split('=')[0] == key)
+                                    exist = true;
+                            })
+                        }
+
+                        if (exist)
+                            return showPopoverMessage(null, null, $table.tableFieldId(index, 'HibProp'), 'Property with such name already exists!');
+
+                        break;
+
+                    case 'sqlFunctionClasses':
+                        if (!$common.isValidJavaClass('SQL function', fx, false, $table.tableFieldId(index, 'SqlFx')))
+                            return $table.tableFocusInvalidField(index, 'SqlFx');
+
+                        model = item[field.model];
+
+                        if ($common.isDefined(model)) {
+                            var idx = _.indexOf(model, fx);
+
+                            // Found duplicate.
+                            if (idx >= 0 && idx != index)
+                                return showPopoverMessage(null, null, $table.tableFieldId(index, 'SqlFx'), 'SQL function with such class name already exists!');
+                        }
+                }
+
+                return true;
+            };
+
+            $scope.tablePairValid = function (item, field, index) {
+                var pairValue = $table.tablePairValue(field, index);
+
+                if (!$common.isValidJavaClass('Indexed type key', pairValue.key, true, $table.tableFieldId(index, 'KeyIndexedType')))
+                    return $table.tableFocusInvalidField(index, 'KeyIndexedType');
+
+                if (!$common.isValidJavaClass('Indexed type value', pairValue.value, true, $table.tableFieldId(index, 'ValueIndexedType')))
+                    return $table.tableFocusInvalidField(index, 'ValueIndexedType');
+
+                var model = item[field.model];
+
+                if ($common.isDefined(model)) {
+                    var idx = _.findIndex(model, function (pair) {
+                        return pair.keyClass == pairValue.key && pair.valueClass == pairValue.value;
+                    });
+
+                    // Found duplicate.
+                    if (idx >= 0 && idx != index)
+                        return showPopoverMessage(null, null, $table.tableFieldId(index, 'ValueIndexedType'), 'Indexed type with such key and value classes already exists!');
+                }
+
+                return true;
+            };
+
+            function selectFirstItem() {
+                if ($scope.caches.length > 0)
+                    $scope.selectItem($scope.caches[0]);
+            }
+
+            function cacheMetadatas(item) {
+                return _.reduce($scope.metadatas, function (memo, meta) {
+                    if (item && _.contains(item.metadatas, meta.value)) {
+                        memo.push(meta.meta);
+                    }
+
+                    return memo;
+                }, []);
+            }
+
+            $loading.start('loadingCachesScreen');
+
+            // When landing on the page, get caches and show them.
+            $http.post('caches/list')
+                .success(function (data) {
+                    var validFilter = $filter('metadatasValidation');
+
+                    $scope.spaces = data.spaces;
+                    $scope.caches = data.caches;
+                    $scope.clusters = data.clusters;
+                    $scope.metadatas = _.sortBy(_.map(validFilter(data.metadatas, true, false), function (meta) {
+                        return {value: meta._id, label: meta.valueType, kind: meta.kind, meta: meta}
+                    }), 'label');
+
+                    // Load page descriptor.
+                    $http.get('/models/caches.json')
+                        .success(function (data) {
+                            $scope.screenTip = data.screenTip;
+                            $scope.moreInfo = data.moreInfo;
+                            $scope.general = data.general;
+                            $scope.advanced = data.advanced;
+
+                            $scope.ui.addGroups(data.general, data.advanced);
+
+                            if ($common.getQueryVariable('new'))
+                                $scope.createItem();
+                            else {
+                                var lastSelectedCache = angular.fromJson(sessionStorage.lastSelectedCache);
+
+                                if (lastSelectedCache) {
+                                    var idx = _.findIndex($scope.caches, function (cache) {
+                                        return cache._id == lastSelectedCache;
+                                    });
+
+                                    if (idx >= 0)
+                                        $scope.selectItem($scope.caches[idx]);
+                                    else {
+                                        sessionStorage.removeItem('lastSelectedCache');
+
+                                        selectFirstItem();
+                                    }
+
+                                }
+                                else
+                                    selectFirstItem();
+                            }
+
+                            $scope.$watch('backupItem', function (val) {
+                                if (val) {
+                                    var srcItem = $scope.selectedItem ? $scope.selectedItem : prepareNewItem();
+
+                                    $scope.ui.checkDirty(val, srcItem);
+
+                                    var metas = cacheMetadatas(val);
+                                    var varName = $commonUtils.toJavaName('cache', val.name);
+
+                                    $scope.preview.general.xml = $generatorXml.cacheMetadatas(metas, $generatorXml.cacheGeneral(val)).asString();
+                                    $scope.preview.general.java = $generatorJava.cacheMetadatas(metas, varName, $generatorJava.cacheGeneral(val, varName)).asString();
+                                    $scope.preview.general.allDefaults = $common.isEmptyString($scope.preview.general.xml);
+
+                                    $scope.preview.memory.xml = $generatorXml.cacheMemory(val).asString();
+                                    $scope.preview.memory.java = $generatorJava.cacheMemory(val, varName).asString();
+                                    $scope.preview.memory.allDefaults = $common.isEmptyString($scope.preview.memory.xml);
+
+                                    $scope.preview.query.xml = $generatorXml.cacheQuery(val).asString();
+                                    $scope.preview.query.java = $generatorJava.cacheQuery(val, varName).asString();
+                                    $scope.preview.query.allDefaults = $common.isEmptyString($scope.preview.query.xml);
+
+                                    $scope.preview.store.xml = $generatorXml.cacheStore(val).asString();
+                                    $scope.preview.store.java = $generatorJava.cacheStore(val, varName).asString();
+                                    $scope.preview.store.allDefaults = $common.isEmptyString($scope.preview.store.xml);
+
+                                    $scope.preview.concurrency.xml = $generatorXml.cacheConcurrency(val).asString();
+                                    $scope.preview.concurrency.java = $generatorJava.cacheConcurrency(val, varName).asString();
+                                    $scope.preview.concurrency.allDefaults = $common.isEmptyString($scope.preview.concurrency.xml);
+
+                                    $scope.preview.rebalance.xml = $generatorXml.cacheRebalance(val).asString();
+                                    $scope.preview.rebalance.java = $generatorJava.cacheRebalance(val, varName).asString();
+                                    $scope.preview.rebalance.allDefaults = $common.isEmptyString($scope.preview.rebalance.xml);
+
+                                    $scope.preview.serverNearCache.xml = $generatorXml.cacheServerNearCache(val).asString();
+                                    $scope.preview.serverNearCache.java = $generatorJava.cacheServerNearCache(val, varName).asString();
+                                    $scope.preview.serverNearCache.allDefaults = $common.isEmptyString($scope.preview.serverNearCache.xml);
+
+                                    $scope.preview.statistics.xml = $generatorXml.cacheStatistics(val).asString();
+                                    $scope.preview.statistics.java = $generatorJava.cacheStatistics(val, varName).asString();
+                                    $scope.preview.statistics.allDefaults = $common.isEmptyString($scope.preview.statistics.xml);
+                                }
+                            }, true);
+
+                            $scope.$watch('backupItem.metadatas', function (val) {
+                                var item = $scope.backupItem;
+
+                                var cacheStoreFactory = $common.isDefined(item) &&
+                                    $common.isDefined(item.cacheStoreFactory) &&
+                                    $common.isDefined(item.cacheStoreFactory.kind);
+
+                                if (val && !cacheStoreFactory) {
+                                    if (_.findIndex(cacheMetadatas(item), $common.metadataForStoreConfigured) >= 0) {
+                                        item.cacheStoreFactory.kind = 'CacheJdbcPojoStoreFactory';
+
+                                        if (!item.readThrough && !item.writeThrough) {
+                                            item.readThrough = true;
+                                            item.writeThrough = true;
+                                        }
+
+                                        $timeout(function () {
+                                            $common.ensureActivePanel($scope.panels, 'store');
+                                        });
+                                    }
+                                }
+                            }, true);
+                        })
+                        .error(function (errMsg) {
+                            $common.showError(errMsg);
+                        });
+                })
+                .error(function (errMsg) {
+                    $common.showError(errMsg);
+                })
+                .finally(function () {
+                    $scope.ui.ready = true;
+                    $loading.finish('loadingCachesScreen');
+                });
+
+            $scope.selectItem = function (item, backup) {
+                function selectItem() {
+                    $table.tableReset();
+
+                    $scope.selectedItem = angular.copy(item);
+
+                    try {
+                        if (item)
+                            sessionStorage.lastSelectedCache = angular.toJson(item._id);
+                        else
+                            sessionStorage.removeItem('lastSelectedCache');
+                    }
+                    catch (error) { }
+
+                    _.forEach(previews, function(preview) {
+                        preview.attractAttention = false;
+                    });
+
+                    if (backup)
+                        $scope.backupItem = backup;
+                    else if (item)
+                        $scope.backupItem = angular.copy(item);
+                    else
+                        $scope.backupItem = undefined;
+                }
+
+                $common.confirmUnsavedChanges($scope.ui.isDirty(), selectItem);
+
+                $scope.ui.formTitle = $common.isDefined($scope.backupItem) && $scope.backupItem._id ?
+                    'Selected cache: ' + $scope.backupItem.name : 'New cache';
+            };
+
+            function prepareNewItem() {
+                return {
+                    space: $scope.spaces[0]._id,
+                    cacheMode: 'PARTITIONED',
+                    atomicityMode: 'ATOMIC',
+                    readFromBackup: true,
+                    copyOnRead: true,
+                    clusters: [],
+                    metadatas: []
+                }
+            }
+
+            // Add new cache.
+            $scope.createItem = function () {
+                $table.tableReset();
+
+                $timeout(function () {
+                    $common.ensureActivePanel($scope.panels, 'general', 'cacheName');
+                });
+
+                $scope.selectItem(undefined, prepareNewItem());
+            };
+
+            // Check cache logical consistency.
+            function validate(item) {
+                if ($common.isEmptyString(item.name))
+                    return showPopoverMessage($scope.panels, 'general', 'cacheName', 'Name should not be empty');
+
+                if (item.memoryMode == 'OFFHEAP_TIERED' && item.offHeapMaxMemory == null)
+                    return showPopoverMessage($scope.panels, 'memory', 'offHeapMaxMemory',
+                        'Off-heap max memory should be specified');
+
+                if (item.memoryMode == 'ONHEAP_TIERED' && item.offHeapMaxMemory > 0 &&
+                        !$common.isDefined(item.evictionPolicy.kind)) {
+                    return showPopoverMessage($scope.panels, 'memory', 'evictionPolicy', 'Eviction policy should not be configured');
+                }
+
+                var cacheStoreFactorySelected = item.cacheStoreFactory && item.cacheStoreFactory.kind;
+
+                if (cacheStoreFactorySelected) {
+                    if (item.cacheStoreFactory.kind == 'CacheJdbcPojoStoreFactory') {
+                        if ($common.isEmptyString(item.cacheStoreFactory.CacheJdbcPojoStoreFactory.dataSourceBean))
+                            return showPopoverMessage($scope.panels, 'store', 'dataSourceBean',
+                                'Data source bean should not be empty');
+
+                        if (!item.cacheStoreFactory.CacheJdbcPojoStoreFactory.dialect)
+                            return showPopoverMessage($scope.panels, 'store', 'dialect',
+                                'Dialect should not be empty');
+                    }
+
+                    if (item.cacheStoreFactory.kind == 'CacheJdbcBlobStoreFactory') {
+                        if ($common.isEmptyString(item.cacheStoreFactory.CacheJdbcBlobStoreFactory.user))
+                            return showPopoverMessage($scope.panels, 'store', 'user',
+                                'User should not be empty');
+
+                        if ($common.isEmptyString(item.cacheStoreFactory.CacheJdbcBlobStoreFactory.dataSourceBean))
+                            return showPopoverMessage($scope.panels, 'store', 'dataSourceBean',
+                                'Data source bean should not be empty');
+                    }
+                }
+
+                if ((item.readThrough || item.writeThrough) && !cacheStoreFactorySelected)
+                    return showPopoverMessage($scope.panels, 'store', 'cacheStoreFactory',
+                        (item.readThrough ? 'Read' : 'Write') + ' through are enabled but store is not configured!');
+
+                if (item.writeBehindEnabled && !cacheStoreFactorySelected)
+                    return showPopoverMessage($scope.panels, 'store', 'cacheStoreFactory',
+                        'Write behind enabled but store is not configured!');
+
+                if (cacheStoreFactorySelected) {
+                    if (!item.readThrough && !item.writeThrough)
+                        return showPopoverMessage($scope.panels, 'store', 'readThrough',
+                            'Store is configured but read/write through are not enabled!');
+
+                    if (item.cacheStoreFactory.kind == 'CacheJdbcPojoStoreFactory') {
+                        if ($common.isDefined(item.metadatas)) {
+                            var metadatas = cacheMetadatas($scope.backupItem);
+
+                            if (_.findIndex(metadatas, $common.metadataForStoreConfigured) < 0)
+                                return showPopoverMessage($scope.panels, 'general', 'metadata',
+                                    'Cache with configured JDBC POJO store factory should contain at least one metadata with store configuration');
+                        }
+                    }
+                }
+
+                return true;
+            }
+
+            // Save cache into database.
+            function save(item) {
+                $http.post('caches/save', item)
+                    .success(function (_id) {
+                        $scope.ui.markPristine();
+
+                        var idx = _.findIndex($scope.caches, function (cache) {
+                            return cache._id == _id;
+                        });
+
+                        if (idx >= 0)
+                            angular.extend($scope.caches[idx], item);
+                        else {
+                            item._id = _id;
+                            $scope.caches.push(item);
+                        }
+
+                        $scope.selectItem(item);
+
+                        $common.showInfo('Cache "' + item.name + '" saved.');
+                    })
+                    .error(function (errMsg) {
+                        $common.showError(errMsg);
+                    });
+            }
+
+            // Save cache.
+            $scope.saveItem = function () {
+                $table.tableReset();
+
+                var item = $scope.backupItem;
+
+                if (validate(item))
+                    save(item);
+            };
+
+            // Save cache with new name.
+            $scope.cloneItem = function () {
+                $table.tableReset();
+
+                if (validate($scope.backupItem))
+                    $clone.confirm($scope.backupItem.name).then(function (newName) {
+                        var item = angular.copy($scope.backupItem);
+
+                        item._id = undefined;
+                        item.name = newName;
+
+                        save(item);
+                    });
+            };
+
+            // Remove cache from db.
+            $scope.removeItem = function () {
+                $table.tableReset();
+
+                var selectedItem = $scope.selectedItem;
+
+                $confirm.confirm('Are you sure you want to remove cache: "' + selectedItem.name + '"?')
+                    .then(function () {
+                            var _id = selectedItem._id;
+
+                            $http.post('caches/remove', {_id: _id})
+                                .success(function () {
+                                    $common.showInfo('Cache has been removed: ' + selectedItem.name);
+
+                                    var caches = $scope.caches;
+
+                                    var idx = _.findIndex(caches, function (cache) {
+                                        return cache._id == _id;
+                                    });
+
+                                    if (idx >= 0) {
+                                        caches.splice(idx, 1);
+
+                                        if (caches.length > 0)
+                                            $scope.selectItem(caches[0]);
+                                        else
+                                            $scope.selectItem(undefined, undefined);
+                                    }
+                                })
+                                .error(function (errMsg) {
+                                    $common.showError(errMsg);
+                                });
+                    });
+            };
+
+            // Remove all caches from db.
+            $scope.removeAllItems = function () {
+                $table.tableReset();
+
+                $confirm.confirm('Are you sure you want to remove all caches?')
+                    .then(function () {
+                            $http.post('caches/remove/all')
+                                .success(function () {
+                                    $common.showInfo('All caches have been removed');
+
+                                    $scope.caches = [];
+
+                                    $scope.selectItem(undefined, undefined);
+                                })
+                                .error(function (errMsg) {
+                                    $common.showError(errMsg);
+                                });
+                    });
+            };
+
+            $scope.resetItem = function (group) {
+                var resetTo = $scope.selectedItem;
+
+                if (!$common.isDefined(resetTo))
+                    resetTo = prepareNewItem();
+
+                $common.resetItem($scope.backupItem, resetTo, $scope.general, group);
+                $common.resetItem($scope.backupItem, resetTo, $scope.advanced, group);
+            }
+        }]
+);