You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ag...@apache.org on 2017/04/18 09:00:53 UTC

[41/50] [abbrv] ignite git commit: IGNITE-4995 Multi-cluster support for Web Console.

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/backend/app/browser.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/app/browser.js b/modules/web-console/backend/app/browser.js
deleted file mode 100644
index e9266a8..0000000
--- a/modules/web-console/backend/app/browser.js
+++ /dev/null
@@ -1,539 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-'use strict';
-
-// Fire me up!
-
-/**
- * Module interaction with browsers.
- */
-module.exports = {
-    implements: 'browser-manager',
-    inject: ['require(lodash)', 'require(socket.io)', 'agent-manager', 'configure']
-};
-
-module.exports.factory = (_, socketio, agentMgr, configure) => {
-    const _errorToJson = (err) => {
-        return {
-            message: err.message || err,
-            code: err.code || 1
-        };
-    };
-
-    return {
-        attach: (server) => {
-            const io = socketio(server);
-
-            configure.socketio(io);
-
-            io.sockets.on('connection', (socket) => {
-                const user = socket.request.user;
-
-                const demo = socket.request._query.IgniteDemoMode === 'true';
-
-                const accountId = () => user._id;
-
-                // Return available drivers to browser.
-                socket.on('schemaImport:drivers', (cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.availableDrivers())
-                        .then((drivers) => cb(null, drivers))
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Return schemas from database to browser.
-                socket.on('schemaImport:schemas', (preset, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => {
-                            const jdbcInfo = {user: preset.user, password: preset.password};
-
-                            return agent.metadataSchemas(preset.jdbcDriverJar, preset.jdbcDriverClass, preset.jdbcUrl, jdbcInfo);
-                        })
-                        .then((schemas) => cb(null, schemas))
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Return tables from database to browser.
-                socket.on('schemaImport:tables', (preset, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => {
-                            const jdbcInfo = {user: preset.user, password: preset.password};
-
-                            return agent.metadataTables(preset.jdbcDriverJar, preset.jdbcDriverClass, preset.jdbcUrl, jdbcInfo,
-                                preset.schemas, preset.tablesOnly);
-                        })
-                        .then((tables) => cb(null, tables))
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Return topology command result from grid to browser.
-                socket.on('node:topology', (attr, mtr, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.topology(demo, attr, mtr))
-                        .then((clusters) => cb(null, clusters))
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Close query on node.
-                socket.on('node:query:close', (nid, queryId, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.queryClose(demo, nid, queryId))
-                        .then(() => cb())
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Execute query on node and return first page to browser.
-                socket.on('node:query', (nid, cacheName, query, distributedJoins, enforceJoinOrder, local, pageSize, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.fieldsQuery(demo, nid, cacheName, query, distributedJoins, enforceJoinOrder, local, pageSize))
-                        .then((res) => cb(null, res))
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Fetch next page for query and return result to browser.
-                socket.on('node:query:fetch', (nid, queryId, pageSize, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.queryFetch(demo, nid, queryId, pageSize))
-                        .then((res) => cb(null, res))
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                const fetchResult = (acc) => {
-                    if (!acc.hasMore)
-                        return acc;
-
-                    return agent.queryFetch(demo, acc.responseNodeId, acc.queryId, pageSize)
-                        .then(({result}) => {
-                            acc.rows = acc.rows.concat(result.rows);
-                            acc.hasMore = result.hasMore;
-
-                            return fetchResult(acc);
-                        });
-                };
-
-                // Execute query on node and return full result to browser.
-                socket.on('node:query:getAll', (nid, cacheName, query, distributedJoins, enforceJoinOrder, local, cb) => {
-                    // Set page size for query.
-                    const pageSize = 1024;
-
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => {
-                            const firstPage = agent.fieldsQuery(demo, nid, cacheName, query, distributedJoins, enforceJoinOrder, local, pageSize)
-                                .then(({result}) => {
-                                    if (result.error)
-                                        return Promise.reject(result.error);
-
-                                    return result.result;
-                                });
-
-                            return firstPage
-                                .then(fetchResult);
-                        })
-                        .then((res) => cb(null, res))
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Collect cache query metrics and return result to browser.
-                socket.on('node:query:metrics', (nids, since, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.queryDetailMetrics(demo, nids, since))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Collect cache query metrics and return result to browser.
-                socket.on('node:query:reset:metrics', (nids, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.queryResetDetailMetrics(demo, nids))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Collect running queries from all nodes in grid.
-                socket.on('node:query:running', (duration, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.queryCollectRunning(demo, duration))
-                        .then((data) => {
-
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Cancel running query by query id on node.
-                socket.on('node:query:cancel', (nid, queryId, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.queryCancel(demo, nid, queryId))
-                        .then((data) => {
-
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Execute scan query on node and return first page to browser.
-                socket.on('node:scan', (nid, cacheName, filter, regEx, caseSensitive, near, local, pageSize, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.queryScan(demo, nid, cacheName, filter, regEx, caseSensitive, near, local, pageSize))
-                        .then((res) => cb(null, res))
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Execute scan on node and return full result to browser.
-                socket.on('node:scan:getAll', (nid, cacheName, filter, regEx, caseSensitive, near, local, cb) => {
-                    // Set page size for query.
-                    const pageSize = 1024;
-
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => {
-                            const firstPage = agent.queryScan(demo, nid, cacheName, filter, regEx, caseSensitive, near, local, pageSize)
-                                .then(({result}) => {
-                                    if (result.error)
-                                        return Promise.reject(result.error);
-
-                                    return result.result;
-                                });
-
-                            return firstPage
-                                .then(fetchResult);
-                        })
-                        .then((res) => cb(null, res))
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Return cache metadata from all nodes in grid.
-                socket.on('node:cache:metadata', (cacheName, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.metadata(demo, cacheName))
-                        .then((caches) => {
-                            let types = [];
-
-                            const _compact = (className) => {
-                                return className.replace('java.lang.', '').replace('java.util.', '').replace('java.sql.', '');
-                            };
-
-                            const _typeMapper = (meta, typeName) => {
-                                const maskedName = _.isEmpty(meta.cacheName) ? '<default>' : meta.cacheName;
-
-                                let fields = meta.fields[typeName];
-
-                                let columns = [];
-
-                                for (const fieldName in fields) {
-                                    if (fields.hasOwnProperty(fieldName)) {
-                                        const fieldClass = _compact(fields[fieldName]);
-
-                                        columns.push({
-                                            type: 'field',
-                                            name: fieldName,
-                                            clazz: fieldClass,
-                                            system: fieldName === '_KEY' || fieldName === '_VAL',
-                                            cacheName: meta.cacheName,
-                                            typeName,
-                                            maskedName
-                                        });
-                                    }
-                                }
-
-                                const indexes = [];
-
-                                for (const index of meta.indexes[typeName]) {
-                                    fields = [];
-
-                                    for (const field of index.fields) {
-                                        fields.push({
-                                            type: 'index-field',
-                                            name: field,
-                                            order: index.descendings.indexOf(field) < 0,
-                                            unique: index.unique,
-                                            cacheName: meta.cacheName,
-                                            typeName,
-                                            maskedName
-                                        });
-                                    }
-
-                                    if (fields.length > 0) {
-                                        indexes.push({
-                                            type: 'index',
-                                            name: index.name,
-                                            children: fields,
-                                            cacheName: meta.cacheName,
-                                            typeName,
-                                            maskedName
-                                        });
-                                    }
-                                }
-
-                                columns = _.sortBy(columns, 'name');
-
-                                if (!_.isEmpty(indexes)) {
-                                    columns = columns.concat({
-                                        type: 'indexes',
-                                        name: 'Indexes',
-                                        cacheName: meta.cacheName,
-                                        typeName,
-                                        maskedName,
-                                        children: indexes
-                                    });
-                                }
-
-                                return {
-                                    type: 'type',
-                                    cacheName: meta.cacheName || '',
-                                    typeName,
-                                    maskedName,
-                                    children: columns
-                                };
-                            };
-
-                            for (const meta of caches) {
-                                const cacheTypes = meta.types.map(_typeMapper.bind(null, meta));
-
-                                if (!_.isEmpty(cacheTypes))
-                                    types = types.concat(cacheTypes);
-                            }
-
-                            return cb(null, types);
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Fetch next page for query and return result to browser.
-                socket.on('node:visor:collect', (evtOrderKey, evtThrottleCntrKey, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.collect(demo, evtOrderKey, evtThrottleCntrKey))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Gets node configuration for specified node.
-                socket.on('node:configuration', (nid, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.collectNodeConfiguration(demo, nid))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Gets cache configurations for specified node and caches deployment IDs.
-                socket.on('cache:configuration', (nid, caches, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.collectCacheConfigurations(demo, nid, caches))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Reset metrics specified cache on specified node and return result to browser.
-                socket.on('node:cache:reset:metrics', (nid, cacheName, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.cacheResetMetrics(demo, nid, cacheName))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Clear specified cache on specified node and return result to browser.
-                socket.on('node:cache:clear', (nid, cacheName, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.cacheClear(demo, nid, cacheName))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Start specified cache and return result to browser.
-                socket.on('node:cache:start', (nids, near, cacheName, cfg, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.cacheStart(demo, nids, near, cacheName, cfg))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Stop specified cache on specified node and return result to browser.
-                socket.on('node:cache:stop', (nid, cacheName, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.cacheStop(demo, nid, cacheName))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-
-                // Ping node and return result to browser.
-                socket.on('node:ping', (taskNid, nid, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.ping(demo, taskNid, nid))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // GC node and return result to browser.
-                socket.on('node:gc', (nids, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.gc(demo, nids))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Thread dump for node.
-                socket.on('node:thread:dump', (nid, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.threadDump(demo, nid))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Collect cache partitions.
-                socket.on('node:cache:partitions', (nids, cacheName, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.partitions(demo, nids, cacheName))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Stops given node IDs
-                socket.on('node:stop', (nids, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.stopNodes(demo, nids))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Restarts given node IDs.
-                socket.on('node:restart', (nids, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.restartNodes(demo, nids))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Collect service information from grid.
-                socket.on('service:collect', (nid, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.services(demo, nid))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                // Collect service information from grid.
-                socket.on('service:cancel', (nid, name, cb) => {
-                    agentMgr.findAgent(accountId())
-                        .then((agent) => agent.serviceCancel(demo, nid, name))
-                        .then((data) => {
-                            if (data.finished)
-                                return cb(null, data.result);
-
-                            cb(_errorToJson(data.error));
-                        })
-                        .catch((err) => cb(_errorToJson(err)));
-                });
-
-                const count = agentMgr.addAgentListener(user._id, socket);
-
-                socket.emit('agent:count', {count});
-            });
-
-            // Handle browser disconnect event.
-            io.sockets.on('disconnect', (socket) =>
-                agentMgr.removeAgentListener(socket.client.request.user._id, socket)
-            );
-        }
-    };
-};

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/backend/app/browsersHandler.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/app/browsersHandler.js b/modules/web-console/backend/app/browsersHandler.js
new file mode 100644
index 0000000..793fd5b
--- /dev/null
+++ b/modules/web-console/backend/app/browsersHandler.js
@@ -0,0 +1,279 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+// Fire me up!
+
+/**
+ * Module interaction with browsers.
+ */
+module.exports = {
+    implements: 'browsers-handler',
+    inject: ['require(lodash)', 'require(socket.io)', 'configure', 'errors']
+};
+
+module.exports.factory = (_, socketio, configure, errors) => {
+    class BrowserSockets {
+        constructor() {
+            this.sockets = new Map();
+
+            this.agentHnd = null;
+        }
+
+        /**
+         * @param {Socket} sock
+         */
+        add(sock) {
+            const token = sock.request.user.token;
+
+            if (this.sockets.has(token))
+                this.sockets.get(token).push(sock);
+            else
+                this.sockets.set(token, [sock]);
+
+            return this.sockets.get(token);
+        }
+
+        /**
+         * @param {Socket} sock
+         */
+        remove(sock) {
+            const token = sock.request.user.token;
+
+            const sockets = this.sockets.get(token);
+
+            _.pull(sockets, sock);
+
+            return sockets;
+        }
+
+        get(token) {
+            if (this.sockets.has(token))
+                return this.sockets.get(token);
+
+            return [];
+        }
+
+        demo(token) {
+            return _.filter(this.sockets.get(token), (sock) => sock.request._query.IgniteDemoMode === 'true');
+        }
+    }
+
+    return class BrowsersHandler {
+        /**
+         * @constructor
+         */
+        constructor() {
+            /**
+             * Connected browsers.
+             * @type {BrowserSockets}
+             */
+            this._browserSockets = new BrowserSockets();
+
+            /**
+             * Registered Visor task.
+             * @type {Map}
+             */
+            this._visorTasks = new Map();
+        }
+
+        /**
+         * @param {Error} err
+         * @return {{code: number, message: *}}
+         */
+        errorTransformer(err) {
+            return {
+                code: err.code || 1,
+                message: err.message || err
+            };
+        }
+
+        /**
+         * @param {String} token
+         * @param {Array.<Socket>} [socks]
+         */
+        agentStats(token, socks = this._browserSockets.get(token)) {
+            return this._agentHnd.agents(token)
+                .then((agentSocks) => {
+                    const stat = _.reduce(agentSocks, (acc, agentSock) => {
+                        acc.count += 1;
+                        acc.hasDemo |= _.get(agentSock, 'demo.enabled');
+
+                        if (agentSock.cluster) {
+                            acc.clusters.add({
+                                id: agentSock.cluster.id
+                            });
+                        }
+
+                        return acc;
+                    }, {count: 0, hasDemo: false, clusters: new Set()});
+
+                    stat.clusters = Array.from(stat.clusters);
+
+                    return stat;
+                })
+                .catch(() => ({count: 0, hasDemo: false, clusters: []}))
+                .then((stat) => _.forEach(socks, (sock) => sock.emit('agents:stat', stat)));
+        }
+
+        executeOnAgent(token, demo, event, ...args) {
+            const cb = _.last(args);
+
+            return this._agentHnd.agent(token, demo)
+                .then((agentSock) => agentSock.emitEvent(event, ..._.dropRight(args)))
+                .then((res) => cb(null, res))
+                .catch((err) => cb(this.errorTransformer(err)));
+        }
+
+        agentListeners(sock) {
+            const demo = sock.request._query.IgniteDemoMode === 'true';
+            const token = () => sock.request.user.token;
+
+            // Return available drivers to browser.
+            sock.on('schemaImport:drivers', (...args) => {
+                this.executeOnAgent(token(), demo, 'schemaImport:drivers', ...args);
+            });
+
+            // Return schemas from database to browser.
+            sock.on('schemaImport:schemas', (...args) => {
+                this.executeOnAgent(token(), demo, 'schemaImport:schemas', ...args);
+            });
+
+            // Return tables from database to browser.
+            sock.on('schemaImport:tables', (...args) => {
+                this.executeOnAgent(token(), demo, 'schemaImport:tables', ...args);
+            });
+        }
+
+        /**
+         * @param {Promise.<AgentSocket>} agent
+         * @param {Boolean} demo
+         * @param {Object.<String, String>} params
+         * @return {Promise.<T>}
+         */
+        executeOnNode(agent, demo, params) {
+            return agent
+                .then((agentSock) => agentSock.emitEvent('node:rest', {uri: 'ignite', demo, params, method: 'GET'}))
+                .then((res) => {
+                    if (res.status === 0)
+                        return JSON.parse(res.data);
+
+                    throw new Error(res.error);
+                });
+        }
+
+        registerVisorTask(taskId, taskCls, ...argCls) {
+            this._visorTasks.set(taskId, {
+                taskCls,
+                argCls
+            });
+        }
+
+        nodeListeners(sock) {
+            // Return command result from grid to browser.
+            sock.on('node:rest', (clusterId, params, cb) => {
+                const demo = sock.request._query.IgniteDemoMode === 'true';
+                const token = sock.request.user.token;
+
+                const agent = this._agentHnd.agent(token, demo, clusterId);
+
+                this.executeOnNode(agent, demo, params)
+                    .then((data) => cb(null, data))
+                    .catch((err) => cb(this.errorTransformer(err)));
+            });
+
+            const internalVisor = (postfix) => `org.apache.ignite.internal.visor.${postfix}`;
+
+            this.registerVisorTask('querySql', internalVisor('query.VisorQueryTask'), internalVisor('query.VisorQueryArg'));
+            this.registerVisorTask('queryScan', internalVisor('query.VisorScanQueryTask'), internalVisor('query.VisorScanQueryArg'));
+            this.registerVisorTask('queryFetch', internalVisor('query.VisorQueryNextPageTask'), internalVisor('query.VisorQueryNextPageTaskArg'));
+            this.registerVisorTask('queryClose', internalVisor('query.VisorQueryCleanupTask'),
+                'java.util.Map', 'java.util.UUID', 'java.util.Set');
+
+            // Return command result from grid to browser.
+            sock.on('node:visor', (clusterId, taskId, nids, ...args) => {
+                const demo = sock.request._query.IgniteDemoMode === 'true';
+                const token = sock.request.user.token;
+
+                const cb = _.last(args);
+                args = _.dropRight(args);
+
+                const desc = this._visorTasks.get(taskId);
+
+                if (_.isNil(desc))
+                    return cb(this.errorTransformer(new errors.IllegalArgumentException(`Failed to find Visor task for id: ${taskId}`)));
+
+                const params = {
+                    cmd: 'exe',
+                    name: 'org.apache.ignite.internal.visor.compute.VisorGatewayTask',
+                    p1: nids,
+                    p2: desc.taskCls
+                };
+
+                _.forEach(_.concat(desc.argCls, args), (param, idx) => { params[`p${idx + 3}`] = param; });
+
+                const agent = this._agentHnd.agent(token, demo, clusterId);
+
+                this.executeOnNode(agent, demo, params)
+                    .then((data) => {
+                        if (data.finished)
+                            return cb(null, data.result);
+
+                        cb(this.errorTransformer(data.error));
+                    })
+                    .catch((err) => cb(this.errorTransformer(err)));
+            });
+        }
+
+        /**
+         *
+         * @param server
+         * @param {AgentsHandler} agentHnd
+         */
+        attach(server, agentHnd) {
+            this._agentHnd = agentHnd;
+
+            if (this.io)
+                throw 'Browser server already started!';
+
+            const io = socketio(server);
+
+            configure.socketio(io);
+
+            // Handle browser connect event.
+            io.sockets.on('connection', (sock) => {
+                this._browserSockets.add(sock);
+
+                // Handle browser disconnect event.
+                sock.on('disconnect', () => {
+                    this._browserSockets.remove(sock);
+
+                    const demo = sock.request._query.IgniteDemoMode === 'true';
+
+                    // Stop demo if latest demo tab for this token.
+                    demo && agentHnd.tryStopDemo(sock);
+                });
+
+                this.agentListeners(sock);
+                this.nodeListeners(sock);
+
+                this.agentStats(sock.request.user.token, [sock]);
+            });
+        }
+    };
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/backend/app/routes.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/app/routes.js b/modules/web-console/backend/app/routes.js
index 6b5d052..826407b 100644
--- a/modules/web-console/backend/app/routes.js
+++ b/modules/web-console/backend/app/routes.js
@@ -22,11 +22,11 @@
 module.exports = {
     implements: 'routes',
     inject: ['routes/public', 'routes/admin', 'routes/profiles', 'routes/demo', 'routes/clusters', 'routes/domains',
-        'routes/caches', 'routes/igfss', 'routes/notebooks', 'routes/agents', 'routes/configurations', 'routes/activities']
+        'routes/caches', 'routes/igfss', 'routes/notebooks', 'routes/downloads', 'routes/configurations', 'routes/activities']
 };
 
 module.exports.factory = function(publicRoute, adminRoute, profilesRoute, demoRoute,
-    clustersRoute, domainsRoute, cachesRoute, igfssRoute, notebooksRoute, agentsRoute, configurationsRoute, activitiesRoute) {
+    clustersRoute, domainsRoute, cachesRoute, igfssRoute, notebooksRoute, downloadsRoute, configurationsRoute, activitiesRoute) {
     return {
         register: (app) => {
             const _mustAuthenticated = (req, res, next) => {
@@ -58,7 +58,7 @@ module.exports.factory = function(publicRoute, adminRoute, profilesRoute, demoRo
             app.use('/configuration/igfs', igfssRoute);
 
             app.use('/notebooks', _mustAuthenticated, notebooksRoute);
-            app.use('/agent', _mustAuthenticated, agentsRoute);
+            app.use('/downloads', _mustAuthenticated, downloadsRoute);
             app.use('/activities', _mustAuthenticated, activitiesRoute);
         }
     };

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/backend/index.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/index.js b/modules/web-console/backend/index.js
index 27d7298..7416f51 100644
--- a/modules/web-console/backend/index.js
+++ b/modules/web-console/backend/index.js
@@ -32,7 +32,8 @@ try {
     fs.accessSync(igniteModulesInjector, fs.F_OK);
 
     injector = require(igniteModulesInjector);
-} catch (ignore) {
+}
+catch (ignore) {
     injector = require(path.join(__dirname, './injector'));
 }
 
@@ -71,25 +72,36 @@ const _onListening = (addr) => {
     console.log('Start listening on ' + bind);
 };
 
-Promise.all([injector('settings'), injector('app'), injector('agent-manager'), injector('browser-manager')])
-    .then(([settings, app, agentMgr, browserMgr]) => {
-        // Start rest server.
-        const server = settings.server.SSLOptions
-            ? https.createServer(settings.server.SSLOptions) : http.createServer();
+/**
+ * @param settings
+ * @param {ApiServer} apiSrv
+ * @param {AgentsHandler} agentsHnd
+ * @param {BrowsersHandler} BrowsersHandler
+ */
+const init = ([settings, apiSrv, agentsHnd, BrowsersHandler]) => {
+    // Start rest server.
+    const srv = settings.server.SSLOptions ? https.createServer(settings.server.SSLOptions) : http.createServer();
+
+    srv.listen(settings.server.port);
 
-        server.listen(settings.server.port);
-        server.on('error', _onError.bind(null, settings.server.port));
-        server.on('listening', _onListening.bind(null, server.address()));
+    srv.on('error', _onError.bind(null, settings.server.port));
+    srv.on('listening', _onListening.bind(null, srv.address()));
 
-        app.listen(server);
+    apiSrv.attach(srv);
 
-        agentMgr.attach(server);
-        browserMgr.attach(server);
+    const browsersHnd = new BrowsersHandler();
+
+    agentsHnd.attach(srv, browsersHnd);
+    browsersHnd.attach(srv, agentsHnd);
+
+    // Used for automated test.
+    if (process.send)
+        process.send('running');
+};
 
-        // Used for automated test.
-        if (process.send)
-            process.send('running');
-    }).catch((err) => {
+Promise.all([injector('settings'), injector('api-server'), injector('agents-handler'), injector('browsers-handler')])
+    .then(init)
+    .catch((err) => {
         console.error(err);
 
         process.exit(1);

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/backend/package.json
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/package.json b/modules/web-console/backend/package.json
index d50136f..2af7787 100644
--- a/modules/web-console/backend/package.json
+++ b/modules/web-console/backend/package.json
@@ -29,34 +29,34 @@
     "win32"
   ],
   "dependencies": {
-    "body-parser": "^1.15.0",
-    "connect-mongo": "^1.1.0",
+    "body-parser": "1.17.1",
+    "connect-mongo": "1.3.2",
     "cookie-parser": "~1.4.0",
-    "express": "^4.14.0",
-    "express-session": "^1.12.0",
-    "fire-up": "^1.0.0",
-    "glob": "^7.0.3",
-    "jszip": "^3.0.0",
-    "lodash": "^4.8.2",
-    "mongoose": "^4.4.11",
-    "morgan": "^1.7.0",
-    "nconf": "^0.8.2",
-    "nodemailer": "^2.3.0",
-    "passport": "^0.3.2",
-    "passport-local": "^1.0.0",
-    "passport-local-mongoose": "^4.0.0",
-    "passport.socketio": "^3.6.1",
-    "socket.io": "^1.4.5"
+    "express": "4.15.2",
+    "express-session": "1.15.2",
+    "fire-up": "1.0.0",
+    "glob": "7.1.1",
+    "jszip": "3.1.3",
+    "lodash": "4.17.4",
+    "mongoose": "4.9.4",
+    "morgan": "1.8.1",
+    "nconf": "0.8.4",
+    "nodemailer": "3.1.4",
+    "passport": "0.3.2",
+    "passport-local": "1.0.0",
+    "passport-local-mongoose": "4.0.0",
+    "passport.socketio": "3.7.0",
+    "socket.io": "1.7.3"
   },
   "devDependencies": {
-    "chai": "^3.5.0",
-    "cross-env": "^1.0.7",
-    "eslint": "^2.9.0",
-    "eslint-friendly-formatter": "^2.0.5",
-    "jasmine-core": "^2.4.1",
-    "mocha": "~2.5.3",
-    "mocha-teamcity-reporter": "^1.0.0",
-    "mockgoose": "^6.0.6",
-    "supertest": "^2.0.0"
+    "chai": "3.5.0",
+    "cross-env": "4.0.0",
+    "eslint": "3.19.0",
+    "eslint-friendly-formatter": "2.0.7",
+    "jasmine-core": "2.5.2",
+    "mocha": "3.2.0",
+    "mocha-teamcity-reporter": "1.1.1",
+    "mockgoose": "6.0.8",
+    "supertest": "3.0.0"
   }
 }

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/backend/routes/agent.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/routes/agent.js b/modules/web-console/backend/routes/agent.js
deleted file mode 100644
index 5ae807b..0000000
--- a/modules/web-console/backend/routes/agent.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-'use strict';
-
-// Fire me up!
-
-module.exports = {
-    implements: 'routes/agents',
-    inject: ['require(lodash)', 'require(express)', 'services/agents', 'services/activities']
-};
-
-/**
- * @param _
- * @param express
- * @param {AgentsService} agentsService
- * @param {ActivitiesService} activitiesService
- * @returns {Promise}
- */
-module.exports.factory = function(_, express, agentsService, activitiesService) {
-    return new Promise((resolveFactory) => {
-        const router = new express.Router();
-
-        /* Get grid topology. */
-        router.get('/download/zip', (req, res) => {
-            activitiesService.merge(req.user._id, {
-                group: 'agent',
-                action: '/agent/download'
-            });
-
-            agentsService.getArchive(req.origin(), req.user.token)
-                .then(({fileName, buffer}) => {
-                    // Set the archive name.
-                    res.attachment(fileName);
-
-                    res.send(buffer);
-                })
-                .catch(res.api.error);
-        });
-
-        resolveFactory(router);
-    });
-};

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/backend/routes/demo.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/routes/demo.js b/modules/web-console/backend/routes/demo.js
index 3f4166d..b200d83 100644
--- a/modules/web-console/backend/routes/demo.js
+++ b/modules/web-console/backend/routes/demo.js
@@ -26,10 +26,20 @@ const igfss = require('./demo/igfss.json');
 
 module.exports = {
     implements: 'routes/demo',
-    inject: ['require(lodash)', 'require(express)', 'settings', 'mongo', 'services/spaces', 'errors']
+    inject: ['require(lodash)', 'require(express)', 'errors', 'settings', 'mongo', 'services/spaces']
 };
 
-module.exports.factory = (_, express, settings, mongo, spacesService, errors) => {
+/**
+ *
+ * @param _
+ * @param express
+ * @param errors
+ * @param settings
+ * @param mongo
+ * @param spacesService
+ * @return {Promise}
+ */
+module.exports.factory = (_, express, errors, settings, mongo, spacesService) => {
     return new Promise((factoryResolve) => {
         const router = new express.Router();
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/backend/routes/downloads.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/routes/downloads.js b/modules/web-console/backend/routes/downloads.js
new file mode 100644
index 0000000..88a1923
--- /dev/null
+++ b/modules/web-console/backend/routes/downloads.js
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'routes/downloads',
+    inject: ['require(lodash)', 'require(express)', 'services/agents', 'services/activities']
+};
+
+/**
+ * @param _
+ * @param express
+ * @param {DownloadsService} downloadsService
+ * @param {ActivitiesService} activitiesService
+ * @returns {Promise}
+ */
+module.exports.factory = function(_, express, downloadsService, activitiesService) {
+    return new Promise((resolveFactory) => {
+        const router = new express.Router();
+
+        /* Get grid topology. */
+        router.get('/agent', (req, res) => {
+            activitiesService.merge(req.user._id, {
+                group: 'agent',
+                action: '/agent/download'
+            });
+
+            downloadsService.prepareArchive(req.origin(), req.user.token)
+                .then(({fileName, buffer}) => {
+                    // Set the archive name.
+                    res.attachment(fileName);
+
+                    res.send(buffer);
+                })
+                .catch(res.api.error);
+        });
+
+        resolveFactory(router);
+    });
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/backend/services/agents.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/services/agents.js b/modules/web-console/backend/services/agents.js
deleted file mode 100644
index 4931bf8..0000000
--- a/modules/web-console/backend/services/agents.js
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-'use strict';
-
-// Fire me up!
-
-module.exports = {
-    implements: 'services/agents',
-    inject: ['require(lodash)', 'require(fs)', 'require(path)', 'require(jszip)', 'settings', 'agent-manager', 'errors']
-};
-
-/**
- * @param _
- * @param fs
- * @param path
- * @param JSZip
- * @param settings
- * @param agentMgr
- * @param errors
- * @returns {AgentsService}
- */
-module.exports.factory = (_, fs, path, JSZip, settings, agentMgr, errors) => {
-    class AgentsService {
-        /**
-         * Get agent archive with user agent configuration.
-         *
-         * @returns {*} - readable stream for further piping. (http://stuk.github.io/jszip/documentation/api_jszip/generate_node_stream.html)
-         */
-        static getArchive(host, token) {
-            const latest = agentMgr.supportedAgents.latest;
-
-            if (_.isEmpty(latest))
-                throw new errors.MissingResourceException('Missing agent zip on server. Please ask webmaster to upload agent zip!');
-
-            const filePath = latest.filePath;
-            const fileName = latest.fileName;
-
-            const folder = path.basename(latest.fileName, '.zip');
-
-            // Read a zip file.
-            return new Promise((resolve, reject) => {
-                fs.readFile(filePath, (errFs, data) => {
-                    if (errFs)
-                        reject(new errors.ServerErrorException(errFs));
-
-                    JSZip.loadAsync(data)
-                        .then((zip) => {
-                            const prop = [];
-
-                            prop.push('tokens=' + token);
-                            prop.push(`server-uri=${host}`);
-                            prop.push('#Uncomment following options if needed:');
-                            prop.push('#node-uri=http://localhost:8080');
-                            prop.push('#driver-folder=./jdbc-drivers');
-
-                            zip.file(folder + '/default.properties', prop.join('\n'));
-
-                            return zip.generateAsync({type: 'nodebuffer', platform: 'UNIX'})
-                                .then((buffer) => resolve({filePath, fileName, buffer}));
-                        })
-                        .catch(reject);
-                });
-            });
-        }
-    }
-
-    return AgentsService;
-};

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/backend/services/downloads.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/services/downloads.js b/modules/web-console/backend/services/downloads.js
new file mode 100644
index 0000000..3dfc2be
--- /dev/null
+++ b/modules/web-console/backend/services/downloads.js
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'services/agents',
+    inject: ['require(lodash)', 'require(fs)', 'require(path)', 'require(jszip)', 'settings', 'agents-handler', 'errors']
+};
+
+/**
+ * @param _
+ * @param fs
+ * @param path
+ * @param JSZip
+ * @param settings
+ * @param agentsHnd
+ * @param errors
+ * @returns {DownloadsService}
+ */
+module.exports.factory = (_, fs, path, JSZip, settings, agentsHnd, errors) => {
+    class DownloadsService {
+        /**
+         * Get agent archive with user agent configuration.
+         *
+         * @returns {*} - readable stream for further piping. (http://stuk.github.io/jszip/documentation/api_jszip/generate_node_stream.html)
+         */
+        prepareArchive(host, token) {
+            if (_.isEmpty(agentsHnd.currentAgent))
+                throw new errors.MissingResourceException('Missing agent zip on server. Please ask webmaster to upload agent zip!');
+
+            const {filePath, fileName} = agentsHnd.currentAgent;
+
+            const folder = path.basename(fileName, '.zip');
+
+            // Read a zip file.
+            return new Promise((resolve, reject) => {
+                fs.readFile(filePath, (errFs, data) => {
+                    if (errFs)
+                        reject(new errors.ServerErrorException(errFs));
+
+                    JSZip.loadAsync(data)
+                        .then((zip) => {
+                            const prop = [];
+
+                            prop.push(`tokens=${token}`);
+                            prop.push(`server-uri=${host}`);
+                            prop.push('#Uncomment following options if needed:');
+                            prop.push('#node-uri=http://localhost:8080');
+                            prop.push('#driver-folder=./jdbc-drivers');
+
+                            zip.file(`${folder}/default.properties`, prop.join('\n'));
+
+                            return zip.generateAsync({type: 'nodebuffer', platform: 'UNIX'})
+                                .then((buffer) => resolve({filePath, fileName, buffer}));
+                        })
+                        .catch(reject);
+                });
+            });
+        }
+    }
+
+    return new DownloadsService();
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/backend/services/users.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/services/users.js b/modules/web-console/backend/services/users.js
index 0aff45f..51b88e9 100644
--- a/modules/web-console/backend/services/users.js
+++ b/modules/web-console/backend/services/users.js
@@ -21,21 +21,21 @@
 
 module.exports = {
     implements: 'services/users',
-    inject: ['require(lodash)', 'mongo', 'settings', 'services/spaces', 'services/mails', 'services/activities', 'agent-manager', 'errors']
+    inject: ['require(lodash)', 'errors', 'settings', 'mongo', 'services/spaces', 'services/mails', 'services/activities', 'agents-handler']
 };
 
 /**
  * @param _
  * @param mongo
+ * @param errors
  * @param settings
  * @param {SpacesService} spacesService
  * @param {MailsService} mailsService
  * @param {ActivitiesService} activitiesService
- * @param agentMgr
- * @param errors
+ * @param {AgentsHandler} agentHnd
  * @returns {UsersService}
  */
-module.exports.factory = (_, mongo, settings, spacesService, mailsService, activitiesService, agentMgr, errors) => {
+module.exports.factory = (_, errors, settings, mongo, spacesService, mailsService, activitiesService, agentHnd) => {
     const _randomString = () => {
         const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
         const possibleLen = possible.length;
@@ -132,7 +132,7 @@ module.exports.factory = (_, mongo, settings, spacesService, mailsService, activ
                 })
                 .then((user) => {
                     if (changed.token && user.token !== changed.token)
-                        agentMgr.close(user._id, user.token);
+                        agentHnd.onTokenReset(user);
 
                     _.extend(user, changed);
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/backend/test/app/httpAgent.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/test/app/httpAgent.js b/modules/web-console/backend/test/app/httpAgent.js
index 1394dc5..76b191e 100644
--- a/modules/web-console/backend/test/app/httpAgent.js
+++ b/modules/web-console/backend/test/app/httpAgent.js
@@ -21,11 +21,11 @@
 
 module.exports = {
     implements: 'agentFactory',
-    inject: ['app', 'require(http)', 'require(supertest)']
+    inject: ['api-server', 'require(http)', 'require(supertest)']
 };
 
-module.exports.factory = (app, http, request) => {
-    const express = app.listen(http.createServer());
+module.exports.factory = (apiSrv, http, request) => {
+    const express = apiSrv.attach(http.createServer());
     let authAgentInstance = null;
 
     return {

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/backend/test/routes/clusters.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/test/routes/clusters.js b/modules/web-console/backend/test/routes/clusters.js
index 5dd7a60..b9f6565 100644
--- a/modules/web-console/backend/test/routes/clusters.js
+++ b/modules/web-console/backend/test/routes/clusters.js
@@ -53,7 +53,7 @@ suite('routes.clusters', () => {
     });
 
     test('Remove cluster model', (done) => {
-        return agentFactory.authAgent(db.mocks.accounts[0])
+        agentFactory.authAgent(db.mocks.accounts[0])
             .then((agent) => {
                 agent.post('/configuration/clusters/remove')
                     .send({_id: db.mocks.clusters[0]._id})
@@ -68,7 +68,7 @@ suite('routes.clusters', () => {
     });
 
     test('Remove all clusters', (done) => {
-        return agentFactory.authAgent(db.mocks.accounts[0])
+        agentFactory.authAgent(db.mocks.accounts[0])
             .then((agent) => {
                 agent.post('/configuration/clusters/remove/all')
                     .expect(200)

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/frontend/app/app.config.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/app.config.js b/modules/web-console/frontend/app/app.config.js
index 3ca5c3b..cf527e6 100644
--- a/modules/web-console/frontend/app/app.config.js
+++ b/modules/web-console/frontend/app/app.config.js
@@ -42,6 +42,7 @@ igniteConsoleCfg.config(['$animateProvider', ($animateProvider) => {
 igniteConsoleCfg.config(['$modalProvider', ($modalProvider) => {
     angular.extend($modalProvider.defaults, {
         animation: 'am-fade-and-scale',
+        placement: 'center',
         html: true
     });
 }]);

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/frontend/app/app.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/app.js b/modules/web-console/frontend/app/app.js
index 80e32a1..a8460e9 100644
--- a/modules/web-console/frontend/app/app.js
+++ b/modules/web-console/frontend/app/app.js
@@ -114,6 +114,7 @@ import resetPassword from './controllers/reset-password.controller';
 // Components
 import igniteListOfRegisteredUsers from './components/list-of-registered-users';
 import IgniteActivitiesUserDialog from './components/activities-user-dialog';
+import clusterSelect from './components/cluster-select';
 import './components/input-dialog';
 
 // Inject external modules.
@@ -197,6 +198,7 @@ angular
 .directive('igniteOnFocusOut', igniteOnFocusOut)
 .directive('igniteRestoreInputFocus', igniteRestoreInputFocus)
 .directive('igniteListOfRegisteredUsers', igniteListOfRegisteredUsers)
+.directive('igniteClusterSelect', clusterSelect)
 // Services.
 .service('IgniteErrorPopover', ErrorPopover)
 .service('JavaTypes', JavaTypes)
@@ -241,10 +243,10 @@ angular
             abstract: true,
             template: baseTemplate
         })
-        .state('settings', {
+        .state('base.settings', {
             url: '/settings',
             abstract: true,
-            template: baseTemplate
+            template: '<ui-view></ui-view>'
         });
 
     $urlRouterProvider.otherwise('/404');
@@ -256,8 +258,8 @@ angular
     $root.$meta = $meta;
     $root.gettingStarted = gettingStarted;
 }])
-.run(['$rootScope', 'IgniteAgentMonitor', ($root, agentMonitor) => {
-    $root.$on('user', () => agentMonitor.init());
+.run(['$rootScope', 'AgentManager', ($root, agentMgr) => {
+    $root.$on('user', () => agentMgr.connect());
 }])
 .run(['$rootScope', ($root) => {
     $root.$on('$stateChangeStart', () => {
@@ -272,7 +274,7 @@ angular
                 .then((user) => {
                     $root.$broadcast('user', user);
 
-                    $state.go('settings.admin');
+                    $state.go('base.settings.admin');
                 })
                 .then(() => Notebook.load())
                 .catch(Messages.showError);

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/frontend/app/components/activities-user-dialog/index.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/activities-user-dialog/index.js b/modules/web-console/frontend/app/components/activities-user-dialog/index.js
index 2f8fdef..02c7c1e 100644
--- a/modules/web-console/frontend/app/components/activities-user-dialog/index.js
+++ b/modules/web-console/frontend/app/components/activities-user-dialog/index.js
@@ -25,7 +25,6 @@
          resolve: {
              user: () => user
          },
-         placement: 'center',
          controller,
          controllerAs: 'ctrl'
      });

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/frontend/app/components/cluster-select/cluster-select.controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/cluster-select/cluster-select.controller.js b/modules/web-console/frontend/app/components/cluster-select/cluster-select.controller.js
new file mode 100644
index 0000000..a318172
--- /dev/null
+++ b/modules/web-console/frontend/app/components/cluster-select/cluster-select.controller.js
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default ['$scope', 'AgentManager', function($scope, agentMgr) {
+    const ctrl = this;
+
+    ctrl.counter = 1;
+
+    ctrl.cluster = null;
+    ctrl.clusters = [];
+
+    $scope.$watchCollection(() => agentMgr.clusters, (clusters) => {
+        if (_.isEmpty(clusters))
+            return ctrl.clusters.length = 0;
+
+        const removed = _.differenceBy(ctrl.clusters, clusters, 'id');
+
+        if (_.nonEmpty(removed))
+            _.pullAll(ctrl.clusters, removed);
+
+        const added = _.differenceBy(clusters, ctrl.clusters, 'id');
+
+        _.forEach(added, (cluster) => {
+            ctrl.clusters.push({
+                id: cluster.id,
+                name: `Cluster ${cluster.id.substring(0, 8).toUpperCase()}`,
+                click: () => {
+                    if (cluster.id === ctrl.cluster.id)
+                        return;
+
+                    agentMgr.saveToStorage(cluster);
+
+                    window.open(window.location.href, '_blank');
+                }
+            });
+        });
+
+        if (_.isNil(ctrl.cluster))
+            ctrl.cluster = _.find(ctrl.clusters, {id: agentMgr.cluster.id});
+    });
+}];

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/frontend/app/components/cluster-select/cluster-select.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/cluster-select/cluster-select.pug b/modules/web-console/frontend/app/components/cluster-select/cluster-select.pug
new file mode 100644
index 0000000..9c1ab75
--- /dev/null
+++ b/modules/web-console/frontend/app/components/cluster-select/cluster-select.pug
@@ -0,0 +1,40 @@
+//-
+    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.
+
+ul.nav.navbar-nav(ng-if='!IgniteDemoMode' ng-switch='ctrl.clusters.length' style='padding-right: 5px')
+    li.disabled(ng-switch-when='0')
+        a
+            i.icon-cluster
+            label.padding-left-dflt(bs-tooltip='' data-placement='bottom' data-title='Check that Web Agent(s) started and connected to cluster(s)') No clusters available
+
+    li.disabled(ng-switch-when='1')
+        a
+            i.icon-cluster
+            label.padding-left-dflt {{ctrl.cluster.name}}
+
+    li(ng-switch-default)
+        a.dropdown-toggle(bs-dropdown='' data-placement='bottom-left' data-trigger='hover focus' data-container='self' data-animation='' ng-click='$event.stopPropagation()' aria-haspopup='true' aria-expanded='expanded')
+            i.icon-cluster
+            label.padding-left-dflt {{ctrl.cluster.name}}
+            span.caret
+        ul.dropdown-menu(role='menu')
+            li(ng-repeat='item in ctrl.clusters' ng-class='{active: ctrl.cluster === item}')
+                div(ng-click='item.click()')
+                    i.icon-cluster.pull-left(style='margin: 0; padding-left: 10px;')
+                    div: a {{item.name}}
+i.icon-help(bs-tooltip='' data-placement='bottom' data-html=true style='padding-right: 20px; line-height: 25px'
+    data-title='Multi-Cluster Support<br/>\
+        <a href="https://apacheignite-tools.readme.io/docs/multi-cluster-support" target="_blank">More info</a>')

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/frontend/app/components/cluster-select/index.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/cluster-select/index.js b/modules/web-console/frontend/app/components/cluster-select/index.js
new file mode 100644
index 0000000..b73845e
--- /dev/null
+++ b/modules/web-console/frontend/app/components/cluster-select/index.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './cluster-select.pug';
+import controller from './cluster-select.controller';
+
+export default [() => {
+    return {
+        restrict: 'E',
+        template,
+        controller,
+        controllerAs: 'ctrl'
+    };
+}];

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/frontend/app/components/input-dialog/input-dialog.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/input-dialog/input-dialog.service.js b/modules/web-console/frontend/app/components/input-dialog/input-dialog.service.js
index fc3cb85..4a48b46 100644
--- a/modules/web-console/frontend/app/components/input-dialog/input-dialog.service.js
+++ b/modules/web-console/frontend/app/components/input-dialog/input-dialog.service.js
@@ -49,7 +49,6 @@ export default class InputDialog {
                     toValidValue
                 })
             },
-            placement: 'center',
             controller,
             controllerAs: 'ctrl'
         });

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/frontend/app/components/list-of-registered-users/list-of-registered-users.column-defs.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/list-of-registered-users/list-of-registered-users.column-defs.js b/modules/web-console/frontend/app/components/list-of-registered-users/list-of-registered-users.column-defs.js
index 54bfb03..5bacce4 100644
--- a/modules/web-console/frontend/app/components/list-of-registered-users/list-of-registered-users.column-defs.js
+++ b/modules/web-console/frontend/app/components/list-of-registered-users/list-of-registered-users.column-defs.js
@@ -19,7 +19,7 @@ const ICON_SORT = '<span ui-grid-one-bind-id-grid="col.uid + \'-sortdir-text\'"
 
 const USER_TEMPLATE = '<div class="ui-grid-cell-contents"><i class="pull-left" ng-class="row.entity.admin ? \'icon-admin\' : \'icon-user\'"></i>&nbsp;{{ COL_FIELD }}</div>';
 
-const CLUSTER_HEADER_TEMPLATE = `<div class='ui-grid-cell-contents' bs-tooltip data-title='{{ col.headerTooltip(col) }}' data-placement='top'><i class='fa fa-sitemap'></i>${ICON_SORT}</div>`;
+const CLUSTER_HEADER_TEMPLATE = `<div class='ui-grid-cell-contents' bs-tooltip data-title='{{ col.headerTooltip(col) }}' data-placement='top'><i class='icon-cluster'></i>${ICON_SORT}</div>`;
 const MODEL_HEADER_TEMPLATE = `<div class='ui-grid-cell-contents' bs-tooltip data-title='{{ col.headerTooltip(col) }}' data-placement='top'><i class='fa fa-object-group'></i>${ICON_SORT}</div>`;
 const CACHE_HEADER_TEMPLATE = `<div class='ui-grid-cell-contents' bs-tooltip data-title='{{ col.headerTooltip(col) }}' data-placement='top'><i class='fa fa-database'></i>${ICON_SORT}</div>`;
 const IGFS_HEADER_TEMPLATE = `<div class='ui-grid-cell-contents' bs-tooltip data-title='{{ col.headerTooltip(col) }}' data-placement='top'><i class='fa fa-folder-o'></i>${ICON_SORT}</div>`;

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/frontend/app/helpers/jade/form/form-field-text.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/helpers/jade/form/form-field-text.pug b/modules/web-console/frontend/app/helpers/jade/form/form-field-text.pug
index 8444eb4..5a7aa2e 100644
--- a/modules/web-console/frontend/app/helpers/jade/form/form-field-text.pug
+++ b/modules/web-console/frontend/app/helpers/jade/form/form-field-text.pug
@@ -43,7 +43,7 @@ mixin ignite-form-field-text(label, model, name, disabled, required, placeholder
         +ignite-form-field__label(label, name, required)
         .ignite-form-field__control
             +tooltip(tip, tipOpts)
-            
+
             if block
                 block
 
@@ -58,6 +58,23 @@ mixin ignite-form-field-url(label, model, name, required, placeholder, tip)
     .ignite-form-field
         +ignite-form-field__label(label, name, required)
         .ignite-form-field__control
+            +tooltip(tip, tipOpts)
+            
+            if block
+                block
+
+            +form-field-feedback(name, 'required', errLbl + ' could not be empty!')
+            +form-field-feedback(name, 'url', errLbl + ' should be a valid URL!')
+
+            .input-tip
+                +ignite-form-field-url-input(name, model, false, required, placeholder)(attributes=attributes)
+
+mixin ignite-form-field-url(label, model, name, required, placeholder, tip)
+    -var errLbl = label.substring(0, label.length - 1)
+
+    .ignite-form-field
+        +ignite-form-field__label(label, name, required)
+        .ignite-form-field__control
             if tip
                 i.tipField.icon-help(bs-tooltip='' data-title=tip)
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/323e3870/modules/web-console/frontend/app/helpers/jade/mixins.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/helpers/jade/mixins.pug b/modules/web-console/frontend/app/helpers/jade/mixins.pug
index e6990f6..672b8f8 100644
--- a/modules/web-console/frontend/app/helpers/jade/mixins.pug
+++ b/modules/web-console/frontend/app/helpers/jade/mixins.pug
@@ -209,6 +209,12 @@ mixin url(lbl, model, name, required, placeholder, tip)
         if  block
             block
 
+//- Mixin for text field.
+mixin url(lbl, model, name, required, placeholder, tip)
+    +ignite-form-field-url(lbl, model, name, required, placeholder, tip)
+        if  block
+            block
+
 //- Mixin for password field.
 mixin password(lbl, model, name, required, placeholder, tip)
     +ignite-form-field-password(lbl, model, name, false, required, placeholder, tip)