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 2016/09/08 04:46:23 UTC

[06/50] [abbrv] ignite git commit: IGNITE-3706 Added query duration, scan with filter on SQL screen.

IGNITE-3706  Added query duration, scan with filter on SQL screen.


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

Branch: refs/heads/ignite-3629
Commit: 0a1fa244e2f3a17b2962b1e6e8a7e6a01c0a090e
Parents: 9e45dee
Author: Andrey Novikov <an...@apache.org>
Authored: Wed Aug 24 14:54:23 2016 +0700
Committer: Andrey Novikov <an...@apache.org>
Committed: Wed Aug 24 14:54:23 2016 +0700

----------------------------------------------------------------------
 modules/web-console/DEVNOTES.txt                |   4 +-
 modules/web-console/backend/app/agent.js        |  61 +--
 modules/web-console/backend/app/browser.js      |  36 +-
 modules/web-console/frontend/app/app.js         |   2 +
 .../frontend/app/filters/duration.filter.js     |  38 ++
 .../frontend/app/modules/agent/agent.module.js  |  43 +-
 .../app/modules/sql/scan-filter-input.jade      |  39 ++
 .../modules/sql/scan-filter-input.service.js    |  52 ++
 .../frontend/app/modules/sql/sql.controller.js  | 482 +++++++++++--------
 .../frontend/app/modules/sql/sql.module.js      |   2 +
 .../frontend/gulpfile.babel.js/paths.js         |   3 +-
 .../frontend/public/stylesheets/style.scss      |  74 ++-
 modules/web-console/frontend/views/sql/sql.jade |  64 ++-
 13 files changed, 546 insertions(+), 354 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ignite/blob/0a1fa244/modules/web-console/DEVNOTES.txt
----------------------------------------------------------------------
diff --git a/modules/web-console/DEVNOTES.txt b/modules/web-console/DEVNOTES.txt
index 0b35ab0..150041e 100644
--- a/modules/web-console/DEVNOTES.txt
+++ b/modules/web-console/DEVNOTES.txt
@@ -16,7 +16,7 @@ How to deploy locally:
   Check npm version: "npm --version".
 5. Run "npm install --no-optional" in terminal for download dependencies.
 6. Build ignite-web-agent module follow instructions from 'modules/web-agent/README.txt'.
-7. Copy ignite-web-agent-<version>.zip from target of ignite-web-agent module to 'modules/web-console/src/main/js/serve/agent_dists' folder.
+7. Copy ignite-web-agent-<version>.zip from target of ignite-web-agent module to 'modules/web-console/backend/agent_dists' folder.
 
 Steps 1 - 7 should be executed once.
 
@@ -29,6 +29,6 @@ How to run console in development mode:
    If needed run "npm install --no-optional" (if dependencies changed) and run "npm start" to start backend.
 
 3. In new terminal change directory to '$IGNITE_HOME/modules/web-console/frontend'
-  and start webpack in development mode "npm run dev" .
+  and start webpack in development mode "npm run dev".
 
 4. In browser open: http://localhost:9000

http://git-wip-us.apache.org/repos/asf/ignite/blob/0a1fa244/modules/web-console/backend/app/agent.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/app/agent.js b/modules/web-console/backend/app/agent.js
index 0f4eb9f..a1858fd 100644
--- a/modules/web-console/backend/app/agent.js
+++ b/modules/web-console/backend/app/agent.js
@@ -222,59 +222,64 @@ module.exports.factory = function(_, ws, fs, path, JSZip, socketio, settings, mo
         }
 
         /**
-         *
          * @param {Boolean} demo Is need run command on demo node.
+         * @param {String} nid Node id.
          * @param {String} cacheName Cache name.
          * @param {String} query Query.
+         * @param {Boolean} local Flag whether to execute query locally.
          * @param {int} pageSize Page size.
          * @returns {Promise}
          */
-        fieldsQuery(demo, cacheName, query, pageSize) {
-            const cmd = new Command(demo, 'qryfldexe')
-                .addParam('cacheName', cacheName)
-                .addParam('qry', query)
-                .addParam('pageSize', pageSize);
-
-            return this.executeRest(cmd);
-        }
-
-        /**
-         *
-         * @param {Boolean} demo Is need run command on demo node.
-         * @param {String} cacheName Cache name.
-         * @param {int} pageSize Page size.
-         * @returns {Promise}
-         */
-        scan(demo, cacheName, pageSize) {
-            const cmd = new Command(demo, 'qryscanexe')
-                .addParam('cacheName', cacheName)
-                .addParam('pageSize', pageSize);
+        fieldsQuery(demo, nid, cacheName, query, local, pageSize) {
+            const cmd = new Command(demo, 'exe')
+                .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask')
+                .addParam('p1', nid)
+                .addParam('p2', 'org.apache.ignite.internal.visor.query.VisorQueryTask')
+                .addParam('p3', 'org.apache.ignite.internal.visor.query.VisorQueryArg')
+                .addParam('p4', cacheName)
+                .addParam('p5', query)
+                .addParam('p6', local)
+                .addParam('p7', pageSize);
 
             return this.executeRest(cmd);
         }
 
         /**
          * @param {Boolean} demo Is need run command on demo node.
+         * @param {String} nid Node id.
          * @param {int} queryId Query Id.
          * @param {int} pageSize Page size.
          * @returns {Promise}
          */
-        queryFetch(demo, queryId, pageSize) {
-            const cmd = new Command(demo, 'qryfetch')
-                .addParam('qryId', queryId)
-                .addParam('pageSize', pageSize);
+        queryFetch(demo, nid, queryId, pageSize) {
+            const cmd = new Command(demo, 'exe')
+                .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask')
+                .addParam('p1', nid)
+                .addParam('p2', 'org.apache.ignite.internal.visor.query.VisorQueryNextPageTask')
+                .addParam('p3', 'org.apache.ignite.lang.IgniteBiTuple')
+                .addParam('p4', 'java.lang.String')
+                .addParam('p5', 'java.lang.Integer')
+                .addParam('p6', queryId)
+                .addParam('p7', pageSize);
 
             return this.executeRest(cmd);
         }
 
         /**
          * @param {Boolean} demo Is need run command on demo node.
+         * @param {String} nid Node id.
          * @param {int} queryId Query Id.
          * @returns {Promise}
          */
-        queryClose(demo, queryId) {
-            const cmd = new Command(demo, 'qrycls')
-                .addParam('qryId', queryId);
+        queryClose(demo, nid, queryId) {
+            const cmd = new Command(demo, 'exe')
+                .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask')
+                .addParam('p1', '')
+                .addParam('p2', 'org.apache.ignite.internal.visor.query.VisorQueryCleanupTask')
+                .addParam('p3', 'java.util.Map')
+                .addParam('p4', 'java.util.UUID')
+                .addParam('p5', 'java.util.Set')
+                .addParam('p6', `${nid}=${queryId}`);
 
             return this.executeRest(cmd);
         }

http://git-wip-us.apache.org/repos/asf/ignite/blob/0a1fa244/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
index 1c5058d..3256b6a 100644
--- a/modules/web-console/backend/app/browser.js
+++ b/modules/web-console/backend/app/browser.js
@@ -90,53 +90,53 @@ module.exports.factory = (_, socketio, agentMgr, configure) => {
                 });
 
                 // Close query on node.
-                socket.on('node:query:close', (queryId, cb) => {
+                socket.on('node:query:close', (nid, queryId, cb) => {
                     agentMgr.findAgent(accountId())
-                        .then((agent) => agent.queryClose(demo, queryId))
+                        .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', (cacheName, pageSize, query, cb) => {
+                socket.on('node:query', (nid, cacheName, query, local, pageSize, cb) => {
                     agentMgr.findAgent(accountId())
-                        .then((agent) => {
-                            if (query === null)
-                                return agent.scan(demo, cacheName, pageSize);
-
-                            return agent.fieldsQuery(demo, cacheName, query, pageSize);
-                        })
+                        .then((agent) => agent.fieldsQuery(demo, nid, cacheName, query, 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', (queryId, pageSize, cb) => {
+                socket.on('node:query:fetch', (nid, queryId, pageSize, cb) => {
                     agentMgr.findAgent(accountId())
-                        .then((agent) => agent.queryFetch(demo, queryId, pageSize))
+                        .then((agent) => agent.queryFetch(demo, nid, queryId, pageSize))
                         .then((res) => cb(null, res))
                         .catch((err) => cb(_errorToJson(err)));
                 });
 
                 // Execute query on node and return full result to browser.
-                socket.on('node:query:getAll', (cacheName, query, cb) => {
+                socket.on('node:query:getAll', (nid, cacheName, query, local, cb) => {
                     // Set page size for query.
                     const pageSize = 1024;
 
                     agentMgr.findAgent(accountId())
                         .then((agent) => {
-                            const firstPage = query === null ? agent.scan(demo, cacheName, pageSize)
-                                : agent.fieldsQuery(demo, cacheName, query, pageSize);
+                            const firstPage = agent.fieldsQuery(demo, nid, cacheName, query, local, pageSize)
+                                .then(({result}) => {
+                                    if (result.key)
+                                        return Promise.reject(result.key);
+
+                                    return result.value;
+                                });
 
                             const fetchResult = (acc) => {
-                                if (acc.last)
+                                if (!acc.hasMore)
                                     return acc;
 
-                                return agent.queryFetch(demo, acc.queryId, pageSize)
+                                return agent.queryFetch(demo, acc.responseNodeId, acc.queryId, pageSize)
                                     .then((res) => {
-                                        acc.items = acc.items.concat(res.items);
+                                        acc.rows = acc.rows.concat(res.rows);
 
-                                        acc.last = res.last;
+                                        acc.hasMore = res.hasMore;
 
                                         return fetchResult(acc);
                                     });

http://git-wip-us.apache.org/repos/asf/ignite/blob/0a1fa244/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 89381aa..7340d7b 100644
--- a/modules/web-console/frontend/app/app.js
+++ b/modules/web-console/frontend/app/app.js
@@ -89,6 +89,7 @@ import UnsavedChangesGuard from './services/UnsavedChangesGuard.service';
 import byName from './filters/byName.filter';
 import domainsValidation from './filters/domainsValidation.filter';
 import hasPojo from './filters/hasPojo.filter';
+import duration from './filters/duration.filter';
 
 // Generators
 import $generatorCommon from 'generator/generator-common';
@@ -215,6 +216,7 @@ angular
 .filter(...hasPojo)
 .filter(...domainsValidation)
 .filter(...byName)
+.filter(...duration)
 .config(['$stateProvider', '$locationProvider', '$urlRouterProvider', ($stateProvider, $locationProvider, $urlRouterProvider) => {
     // Set up the states.
     $stateProvider

http://git-wip-us.apache.org/repos/asf/ignite/blob/0a1fa244/modules/web-console/frontend/app/filters/duration.filter.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/filters/duration.filter.js b/modules/web-console/frontend/app/filters/duration.filter.js
new file mode 100644
index 0000000..deeedd7
--- /dev/null
+++ b/modules/web-console/frontend/app/filters/duration.filter.js
@@ -0,0 +1,38 @@
+/*
+ * 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 ['duration', [() => {
+    /**
+     * @param {Number} t Time in ms.
+     */
+    return (t) => {
+        const a = (i, suffix) => i && i !== '00' ? i + suffix + ' ' : '';
+
+        const cd = 24 * 60 * 60 * 1000;
+        const ch = 60 * 60 * 1000;
+        const cm = 60 * 1000;
+        const cs = 1000;
+
+        const d = Math.floor(t / cd);
+        const h = Math.floor((t - d * cd) / ch);
+        const m = Math.floor((t - d * cd - h * ch) / cm);
+        const s = Math.floor((t - d * cd - h * ch - m * cm) / cs);
+        const ms = t % 1000;
+
+        return a(d, 'd') + a(h, 'h') + a(m, 'm') + a(s, 's') + (t < cm ? ms + 'ms' : '');
+    };
+}]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/0a1fa244/modules/web-console/frontend/app/modules/agent/agent.module.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/agent/agent.module.js b/modules/web-console/frontend/app/modules/agent/agent.module.js
index ca95166..f65781e 100644
--- a/modules/web-console/frontend/app/modules/agent/agent.module.js
+++ b/modules/web-console/frontend/app/modules/agent/agent.module.js
@@ -18,6 +18,8 @@
 import angular from 'angular';
 import io from 'socket.io-client'; // eslint-disable-line no-unused-vars
 
+const maskNull = (val) => _.isEmpty(val) ? 'null' : val;
+
 class IgniteAgentMonitor {
     constructor(socketFactory, $root, $q, $state, $modal, Messages) {
         this._scope = $root.$new();
@@ -260,47 +262,60 @@ class IgniteAgentMonitor {
     }
 
     /**
+     * @param {String} nid Node id.
      * @param {int} [queryId]
      * @returns {Promise}
      */
-    queryClose(queryId) {
-        return this._rest('node:query:close', queryId);
+    queryClose(nid, queryId) {
+        return this._rest('node:query:close', nid, queryId);
     }
 
     /**
+     * @param {String} nid Node id.
      * @param {String} cacheName Cache name.
-     * @param {int} pageSize
      * @param {String} [query] Query if null then scan query.
+     * @param {Boolean} local Flag whether to execute query locally.
+     * @param {int} pageSize
      * @returns {Promise}
      */
-    query(cacheName, pageSize, query) {
-        return this._rest('node:query', _.isEmpty(cacheName) ? null : cacheName, pageSize, query);
+    query(nid, cacheName, query, local, pageSize) {
+        return this._rest('node:query', nid, maskNull(cacheName), maskNull(query), local, pageSize)
+            .then(({result}) => {
+                if (_.isEmpty(result.key))
+                    return result.value;
+
+                return Promise.reject(result.key);
+            });
     }
 
     /**
+     * @param {String} nid Node id.
      * @param {String} cacheName Cache name.
      * @param {String} [query] Query if null then scan query.
+     * @param {Boolean} local Flag whether to execute query locally.
      * @returns {Promise}
      */
-    queryGetAll(cacheName, query) {
-        return this._rest('node:query:getAll', _.isEmpty(cacheName) ? null : cacheName, query);
+    queryGetAll(nid, cacheName, query, local) {
+        return this._rest('node:query:getAll', nid, maskNull(cacheName), maskNull(query), local);
     }
 
     /**
-     * @param {String} [cacheName] Cache name.
+     * @param {String} nid Node id.
+     * @param {int} queryId
+     * @param {int} pageSize
      * @returns {Promise}
      */
-    metadata(cacheName) {
-        return this._rest('node:cache:metadata', _.isEmpty(cacheName) ? null : cacheName);
+    next(nid, queryId, pageSize) {
+        return this._rest('node:query:fetch', nid, queryId, pageSize)
+            .then(({result}) => result);
     }
 
     /**
-     * @param {int} queryId
-     * @param {int} pageSize
+     * @param {String} [cacheName] Cache name.
      * @returns {Promise}
      */
-    next(queryId, pageSize) {
-        return this._rest('node:query:fetch', queryId, pageSize);
+    metadata(cacheName) {
+        return this._rest('node:cache:metadata', maskNull(cacheName));
     }
 
     stopWatch() {

http://git-wip-us.apache.org/repos/asf/ignite/blob/0a1fa244/modules/web-console/frontend/app/modules/sql/scan-filter-input.jade
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/sql/scan-filter-input.jade b/modules/web-console/frontend/app/modules/sql/scan-filter-input.jade
new file mode 100644
index 0000000..addc5f3
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/sql/scan-filter-input.jade
@@ -0,0 +1,39 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include ../../helpers/jade/mixins.jade
+
+.modal(tabindex='-1' role='dialog')
+    .modal-dialog
+        .modal-content
+            .modal-header
+                button.close(ng-click='$hide()') &times;
+                h4.modal-title Scan filter
+            form.form-horizontal.modal-body.row(name='ui.inputForm' novalidate)
+                .settings-row
+                    .col-sm-2
+                        label.required.labelFormField Filter:&nbsp;
+                    .col-sm-10
+                        .input-tip
+                            +ignite-form-field-input('"filter"', 'ui.filter', false, 'true', 'Enter filter')(
+                                data-ignite-form-field-input-autofocus='true'
+                                ignite-on-enter='form.$valid && ok()'
+                            )
+                .settings-row
+                    +checkbox('Case sensitive search', 'ui.caseSensitive', '"caseSensitive"', 'Select this checkbox for case sensitive search')
+            .modal-footer
+                button.btn.btn-default(id='btn-cancel' ng-click='$hide()') Cancel
+                button.btn.btn-primary(id='btn-scan' ng-disabled='ui.inputForm.$invalid' ng-click='ok()') Scan

http://git-wip-us.apache.org/repos/asf/ignite/blob/0a1fa244/modules/web-console/frontend/app/modules/sql/scan-filter-input.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/sql/scan-filter-input.service.js b/modules/web-console/frontend/app/modules/sql/scan-filter-input.service.js
new file mode 100644
index 0000000..d460f04
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/sql/scan-filter-input.service.js
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+class ScanFilter {
+    static $inject = ['$rootScope', '$q', '$modal'];
+
+    constructor($root, $q, $modal) {
+        this.deferred = null;
+        this.$q = $q;
+
+        const scope = $root.$new();
+
+        scope.ui = {};
+
+        scope.ok = () => {
+            this.deferred.resolve({filter: scope.ui.filter, caseSensitive: !!scope.ui.caseSensitive});
+
+            this.modal.hide();
+        };
+
+        scope.$hide = () => {
+            this.modal.hide();
+
+            this.deferred.reject();
+        };
+
+        this.modal = $modal({templateUrl: '/scan-filter-input.html', scope, placement: 'center', show: false});
+    }
+
+    open() {
+        this.deferred = this.$q.defer();
+
+        this.modal.$promise.then(this.modal.show);
+
+        return this.deferred.promise;
+    }
+}
+export default ScanFilter;

http://git-wip-us.apache.org/repos/asf/ignite/blob/0a1fa244/modules/web-console/frontend/app/modules/sql/sql.controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/sql/sql.controller.js b/modules/web-console/frontend/app/modules/sql/sql.controller.js
index 76db15f..3162224 100644
--- a/modules/web-console/frontend/app/modules/sql/sql.controller.js
+++ b/modules/web-console/frontend/app/modules/sql/sql.controller.js
@@ -15,9 +15,92 @@
  * limitations under the License.
  */
 
+// Time line X axis descriptor.
+const TIME_LINE = {value: -1, type: 'java.sql.Date', label: 'TIME_LINE'};
+
+// Row index X axis descriptor.
+const ROW_IDX = {value: -2, type: 'java.lang.Integer', label: 'ROW_IDX'};
+
+/** Prefix for node local key for SCAN near queries. */
+const SCAN_NEAR_CACHE = 'VISOR_SCAN_NEAR_CACHE';
+
+/** Prefix for node local key for SCAN near queries. */
+const SCAN_CACHE_WITH_FILTER = 'VISOR_SCAN_CACHE_WITH_FILTER';
+
+/** Prefix for node local key for SCAN near queries. */
+const SCAN_CACHE_WITH_FILTER_CASE_SENSITIVE = 'VISOR_SCAN_CACHE_WITH_FILTER_CASE_SENSITIVE';
+
+const _fullColName = (col) => {
+    const res = [];
+
+    if (col.schemaName)
+        res.push(col.schemaName);
+
+    if (col.typeName)
+        res.push(col.typeName);
+
+    res.push(col.fieldName);
+
+    return res.join('.');
+};
+
+class Paragraph {
+    constructor($timeout, Notebook, paragraph) {
+        this.$timeout = $timeout;
+        this.Notebook = Notebook;
+
+        _.assign(this, paragraph);
+    }
+
+    resultType() {
+        if (_.isNil(this.queryArgs))
+            return null;
+
+        if (!_.isEmpty(this.errMsg))
+            return 'error';
+
+        if (_.isEmpty(this.rows))
+            return 'empty';
+
+        return this.result === 'table' ? 'table' : 'chart';
+    }
+
+    nonRefresh() {
+        return _.isNil(this.rate) || _.isNil(this.rate.stopTime);
+    }
+
+    table() {
+        return this.result === 'table';
+    }
+
+    chart() {
+        return this.result !== 'table' && this.result !== 'none';
+    }
+
+    nonEmpty() {
+        return this.rows && this.rows.length > 0;
+    }
+
+    queryExecuted() {
+        return this.queryArgs && this.queryArgs.query && !this.queryArgs.query.startsWith('EXPLAIN ');
+    }
+
+    timeLineSupported() {
+        return this.result !== 'pie';
+    }
+
+    chartColumnsConfigured() {
+        return !_.isEmpty(this.chartKeyCols) && !_.isEmpty(this.chartValCols);
+    }
+
+    chartTimeLineEnabled() {
+        return !_.isEmpty(this.chartKeyCols) && _.eq(this.chartKeyCols[0], TIME_LINE);
+    }
+}
+
 // Controller for SQL notebook screen.
-export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval', '$animate', '$location', '$anchorScroll', '$state', '$modal', '$popover', 'IgniteLoading', 'IgniteLegacyUtils', 'IgniteMessages', 'IgniteConfirm', 'IgniteAgentMonitor', 'IgniteChartColors', 'IgniteNotebook', 'uiGridConstants', 'uiGridExporterConstants',
-    function($root, $scope, $http, $q, $timeout, $interval, $animate, $location, $anchorScroll, $state, $modal, $popover, Loading, LegacyUtils, Messages, Confirm, agentMonitor, IgniteChartColors, Notebook, uiGridConstants, uiGridExporterConstants) {
+export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval', '$animate', '$location', '$anchorScroll', '$state', '$modal', '$popover', 'IgniteLoading', 'IgniteLegacyUtils', 'IgniteMessages', 'IgniteConfirm', 'IgniteAgentMonitor', 'IgniteChartColors', 'IgniteNotebook', 'IgniteScanFilterInput', 'uiGridConstants', 'uiGridExporterConstants',
+    function($root, $scope, $http, $q, $timeout, $interval, $animate, $location, $anchorScroll, $state, $modal, $popover, Loading, LegacyUtils, Messages, Confirm, agentMonitor, IgniteChartColors, Notebook, ScanFilterInput, uiGridConstants, uiGridExporterConstants) {
         let stopTopology = null;
 
         const _tryStopRefresh = function(paragraph) {
@@ -76,39 +159,6 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
 
         $scope.maskCacheName = (cacheName) => _.isEmpty(cacheName) ? '<default>' : cacheName;
 
-        // Time line X axis descriptor.
-        const TIME_LINE = {value: -1, type: 'java.sql.Date', label: 'TIME_LINE'};
-
-        // Row index X axis descriptor.
-        const ROW_IDX = {value: -2, type: 'java.lang.Integer', label: 'ROW_IDX'};
-
-        const EXTENDED_PARAGRAPH = {
-            table() {
-                return this.result === 'table';
-            },
-            chart() {
-                return this.result !== 'table' && this.result !== 'none';
-            },
-            nonEmpty() {
-                return this.rows && this.rows.length > 0;
-            },
-            queryExecuted() {
-                return this.queryArgs && this.queryArgs.query && !this.queryArgs.query.startsWith('EXPLAIN ');
-            },
-            refreshExecuting() {
-                return this.rate && this.rate.stopTime;
-            },
-            timeLineSupported() {
-                return this.result !== 'pie';
-            },
-            chartColumnsConfigured() {
-                return !_.isEmpty(this.chartKeyCols) && !_.isEmpty(this.chartValCols);
-            },
-            chartTimeLineEnabled() {
-                return !_.isEmpty(this.chartKeyCols) && _.eq(this.chartKeyCols[0], TIME_LINE);
-            }
-        };
-
         // We need max 1800 items to hold history for 30 mins in case of refresh every second.
         const HISTORY_LENGTH = 1800;
 
@@ -653,65 +703,6 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
 
         let paragraphId = 0;
 
-        const _fullColName = function(col) {
-            const res = [];
-
-            if (col.schemaName)
-                res.push(col.schemaName);
-
-            if (col.typeName)
-                res.push(col.typeName);
-
-            res.push(col.fieldName);
-
-            return res.join('.');
-        };
-
-        function enhanceParagraph(paragraph) {
-            Object.assign(paragraph, EXTENDED_PARAGRAPH);
-
-            Object.defineProperty(paragraph, 'gridOptions', { value: {
-                enableGridMenu: false,
-                enableColumnMenus: false,
-                flatEntityAccess: true,
-                fastWatch: true,
-                updateColumns(cols) {
-                    this.columnDefs = _.map(cols, (col) => {
-                        return {
-                            displayName: col.fieldName,
-                            headerTooltip: _fullColName(col),
-                            field: col.field,
-                            minWidth: 50,
-                            cellClass: 'cell-left'
-                        };
-                    });
-
-                    $timeout(() => this.api.core.notifyDataChange(uiGridConstants.dataChange.COLUMN));
-                },
-                updateRows(rows) {
-                    const sizeChanged = this.data.length !== rows.length;
-
-                    this.data = rows;
-
-                    if (sizeChanged) {
-                        const height = Math.min(rows.length, 15) * 30 + 47;
-
-                        // Remove header height.
-                        this.api.grid.element.css('height', height + 'px');
-
-                        $timeout(() => this.api.core.handleWindowResize());
-                    }
-                },
-                onRegisterApi(api) {
-                    $animate.enabled(api.grid.element, false);
-
-                    this.api = api;
-                }
-            }});
-
-            Object.defineProperty(paragraph, 'chartHistory', {value: []});
-        }
-
         $scope.aceInit = function(paragraph) {
             return function(editor) {
                 editor.setAutoScrollEditorIntoView(true);
@@ -732,89 +723,142 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
             };
         };
 
-        const _setActiveCache = function() {
-            if ($scope.caches.length > 0) {
-                _.forEach($scope.notebook.paragraphs, (paragraph) => {
-                    if (!_.find($scope.caches, {name: paragraph.cacheName}))
-                        paragraph.cacheName = $scope.caches[0].name;
-                });
-            }
-        };
-
-        const _updateTopology = () =>
+        /**
+         * Update caches list.
+         * @private
+         */
+        const _refreshFn = () =>
             agentMonitor.topology()
                 .then((clusters) => {
-                    agentMonitor.checkModal();
+                    $scope.caches = _.sortBy(_.reduce(clusters, (items, cluster) => {
+                        _.forEach(cluster.caches, (cache) => {
+                            let item = _.find(items, {name: cache.name});
 
-                    const caches = _.flattenDeep(clusters.map((cluster) => cluster.caches));
+                            if (_.isNil(item)) {
+                                cache.label = $scope.maskCacheName(cache.name);
 
-                    $scope.caches = _.sortBy(_.map(_.uniqBy(_.reject(caches, {mode: 'LOCAL'}), 'name'), (cache) => {
-                        cache.label = $scope.maskCacheName(cache.name);
+                                cache.nodeIds = [];
 
-                        return cache;
-                    }), 'label');
+                                items.push(item = cache);
+                            }
+
+                            item.nodeIds.push(cluster.nodeId);
+                        });
+
+                        return items;
+                    }, []), 'label');
+
+                    if (_.isEmpty($scope.caches))
+                        return;
+
+                    // Reset to first cache in case of stopped selected.
+                    const cacheNames = _.map($scope.caches, (cache) => cache.name);
 
-                    _setActiveCache();
+                    _.forEach($scope.notebook.paragraphs, (paragraph) => {
+                        if (!_.includes(cacheNames, paragraph.cacheName))
+                            paragraph.cacheName = _.head(cacheNames);
+                    });
                 })
-                .catch((err) => {
-                    if (err.code === 2)
-                        return agentMonitor.showNodeError('Agent is failed to authenticate in grid. Please check agent\'s login and password.');
+                .then(() => agentMonitor.checkModal())
+                .catch((err) => agentMonitor.showNodeError(err));
+
+        const _startWatch = () =>
+            agentMonitor.startWatch({
+                state: 'base.configuration.clusters',
+                text: 'Back to Configuration',
+                goal: 'execute sql statements',
+                onDisconnect: () => {
+                    _stopTopologyRefresh();
 
-                    agentMonitor.showNodeError(err);
+                    _startWatch();
+                }
+            })
+                .then(() => Loading.start('sqlLoading'))
+                .then(_refreshFn)
+                .then(() => Loading.finish('sqlLoading'))
+                .then(() => {
+                    $root.IgniteDemoMode && _.forEach($scope.notebook.paragraphs, $scope.execute);
+
+                    stopTopology = $interval(_refreshFn, 5000, 0, false);
                 });
 
-        const _startTopologyRefresh = () => {
-            Loading.start('sqlLoading');
+        Notebook.find($state.params.noteId)
+            .then((notebook) => {
+                $scope.notebook = _.cloneDeep(notebook);
+
+                $scope.notebook_name = $scope.notebook.name;
+
+                if (!$scope.notebook.expandedParagraphs)
+                    $scope.notebook.expandedParagraphs = [];
+
+                if (!$scope.notebook.paragraphs)
+                    $scope.notebook.paragraphs = [];
+
+                $scope.notebook.paragraphs = _.map($scope.notebook.paragraphs, (paragraph) => {
+                    paragraph.id = 'paragraph-' + paragraphId++;
+
+                    const par = new Paragraph($timeout, Notebook, paragraph);
+
+                    Object.defineProperty(par, 'gridOptions', {value: {
+                        enableGridMenu: false,
+                        enableColumnMenus: false,
+                        flatEntityAccess: true,
+                        fastWatch: true,
+                        rebuildColumns() {
+                            if (_.isNil(this.api))
+                                return;
+
+                            this.columnDefs = _.reduce(par.meta, (cols, col, idx) => {
+                                if (par.columnFilter(col)) {
+                                    cols.push({
+                                        displayName: col.fieldName,
+                                        headerTooltip: _fullColName(col),
+                                        field: idx.toString(),
+                                        minWidth: 50,
+                                        cellClass: 'cell-left'
+                                    });
+                                }
 
-            agentMonitor.awaitAgent()
-                .then(_updateTopology)
-                .then(() => {
-                    if ($root.IgniteDemoMode)
-                        _.forEach($scope.notebook.paragraphs, $scope.execute);
+                                return cols;
+                            }, []);
 
-                    Loading.finish('sqlLoading');
+                            $timeout(() => this.api.core.notifyDataChange(uiGridConstants.dataChange.COLUMN));
+                        },
+                        adjustHeight() {
+                            if (_.isNil(this.api))
+                                return;
 
-                    stopTopology = $interval(_updateTopology, 5000, 0, false);
-                });
-        };
+                            this.data = par.rows;
 
-        const loadNotebook = function(notebook) {
-            $scope.notebook = _.cloneDeep(notebook);
+                            const height = Math.min(this.data.length, 15) * 30 + 47;
 
-            $scope.notebook_name = $scope.notebook.name;
+                            // Remove header height.
+                            this.api.grid.element.css('height', height + 'px');
 
-            if (!$scope.notebook.expandedParagraphs)
-                $scope.notebook.expandedParagraphs = [];
+                            $timeout(() => this.api.core.handleWindowResize());
+                        },
+                        onRegisterApi(api) {
+                            $animate.enabled(api.grid.element, false);
 
-            if (!$scope.notebook.paragraphs)
-                $scope.notebook.paragraphs = [];
+                            this.api = api;
 
-            _.forEach($scope.notebook.paragraphs, (paragraph) => {
-                paragraph.id = 'paragraph-' + paragraphId++;
+                            this.rebuildColumns();
 
-                enhanceParagraph(paragraph);
-            });
+                            this.adjustHeight();
+                        }
+                    }});
 
-            if (_.isEmpty($scope.notebook.paragraphs))
-                $scope.addParagraph();
-            else
-                $scope.rebuildScrollParagraphs();
+                    Object.defineProperty(par, 'chartHistory', {value: []});
 
-            agentMonitor.startWatch({
-                state: 'base.configuration.clusters',
-                text: 'Back to Configuration',
-                goal: 'execute sql statements',
-                onDisconnect: () => {
-                    _stopTopologyRefresh();
+                    return par;
+                });
 
-                    _startTopologyRefresh();
-                }
+                if (_.isEmpty($scope.notebook.paragraphs))
+                    $scope.addParagraph();
+                else
+                    $scope.rebuildScrollParagraphs();
             })
-            .then(_startTopologyRefresh);
-        };
-
-        Notebook.find($state.params.noteId)
-            .then(loadNotebook)
+            .then(_startWatch)
             .catch(() => {
                 $scope.notebookLoadFailed = true;
 
@@ -878,8 +922,6 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
                 }
             };
 
-            enhanceParagraph(paragraph);
-
             if ($scope.caches && $scope.caches.length > 0)
                 paragraph.cacheName = $scope.caches[0].name;
 
@@ -893,9 +935,7 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
 
             $anchorScroll();
 
-            setTimeout(function() {
-                paragraph.ace.focus();
-            });
+            setTimeout(paragraph.ace.focus);
         };
 
         function _saveChartSettings(paragraph) {
@@ -931,8 +971,6 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
 
             if (paragraph.chart())
                 _chartApplySettings(paragraph, true);
-            else
-                $timeout(() => paragraph.gridOptions.api.core.handleWindowResize());
         };
 
         $scope.resultEq = function(paragraph, result) {
@@ -1033,25 +1071,15 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
                 });
             });
 
-            const cols = [];
-
-            _.forEach(paragraph.meta, (col, idx) => {
-                if (paragraph.columnFilter(col)) {
-                    col.field = paragraph.queryArgs.query ? idx.toString() : col.fieldName;
-
-                    cols.push(col);
-                }
-            });
-
-            paragraph.gridOptions.updateColumns(cols);
+            paragraph.gridOptions.rebuildColumns();
 
-            paragraph.chartColumns = _.reduce(cols, (acc, col) => {
-                if (_notObjectType(col.fieldTypeName)) {
+            paragraph.chartColumns = _.reduce(paragraph.meta, (acc, col, idx) => {
+                if (paragraph.columnFilter(col) && _notObjectType(col.fieldTypeName)) {
                     acc.push({
                         label: col.fieldName,
                         type: col.fieldTypeName,
                         aggFx: $scope.aggregateFxs[0],
-                        value: col.field
+                        value: idx.toString()
                     });
                 }
 
@@ -1087,14 +1115,14 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
 
         /**
          * @param {Object} paragraph Query
-         * @param {{fieldsMetadata: Array, items: Array, queryId: int, last: Boolean}} res Query results.
+         * @param {{columns: Array, rows: Array, responseNodeId: String, queryId: int, hasMore: Boolean}} res Query results.
          * @private
          */
-        const _processQueryResult = function(paragraph, res) {
+        const _processQueryResult = (paragraph, res) => {
             const prevKeyCols = paragraph.chartKeyCols;
             const prevValCols = paragraph.chartValCols;
 
-            if (!_.eq(paragraph.meta, res.fieldsMetadata)) {
+            if (!_.eq(paragraph.meta, res.columns)) {
                 paragraph.meta = [];
 
                 paragraph.chartColumns = [];
@@ -1105,17 +1133,17 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
                 if (!LegacyUtils.isDefined(paragraph.chartValCols))
                     paragraph.chartValCols = [];
 
-                if (res.fieldsMetadata.length <= 2) {
-                    const _key = _.find(res.fieldsMetadata, {fieldName: '_KEY'});
-                    const _val = _.find(res.fieldsMetadata, {fieldName: '_VAL'});
+                if (res.columns.length <= 2) {
+                    const _key = _.find(res.columns, {fieldName: '_KEY'});
+                    const _val = _.find(res.columns, {fieldName: '_VAL'});
 
-                    paragraph.disabledSystemColumns = (res.fieldsMetadata.length === 2 && _key && _val) ||
-                        (res.fieldsMetadata.length === 1 && (_key || _val));
+                    paragraph.disabledSystemColumns = (res.columns.length === 2 && _key && _val) ||
+                        (res.columns.length === 1 && (_key || _val));
                 }
 
                 paragraph.columnFilter = _columnFilter(paragraph);
 
-                paragraph.meta = res.fieldsMetadata;
+                paragraph.meta = res.columns;
 
                 _rebuildColumns(paragraph);
             }
@@ -1124,24 +1152,28 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
 
             paragraph.total = 0;
 
-            paragraph.queryId = res.last ? null : res.queryId;
+            paragraph.duration = res.duration;
+
+            paragraph.queryId = res.hasMore ? res.queryId : null;
+
+            paragraph.resNodeId = res.responseNodeId;
 
             delete paragraph.errMsg;
 
             // Prepare explain results for display in table.
-            if (paragraph.queryArgs.query && paragraph.queryArgs.query.startsWith('EXPLAIN') && res.items) {
+            if (paragraph.queryArgs.query && paragraph.queryArgs.query.startsWith('EXPLAIN') && res.rows) {
                 paragraph.rows = [];
 
-                res.items.forEach(function(row, i) {
-                    const line = res.items.length - 1 === i ? row[0] : row[0] + '\n';
+                res.rows.forEach((row, i) => {
+                    const line = res.rows.length - 1 === i ? row[0] : row[0] + '\n';
 
                     line.replace(/\"/g, '').split('\n').forEach((ln) => paragraph.rows.push([ln]));
                 });
             }
             else
-                paragraph.rows = res.items;
+                paragraph.rows = res.rows;
 
-            paragraph.gridOptions.updateRows(paragraph.rows);
+            paragraph.gridOptions.adjustHeight(paragraph.rows.length);
 
             const chartHistory = paragraph.chartHistory;
 
@@ -1189,12 +1221,18 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
             return queryId ? agentMonitor.queryClose(queryId) : $q.when();
         };
 
+        const cacheNode = (name) => {
+            const cache = _.find($scope.caches, {name});
+
+            return cache.nodeIds[_.random(0, cache.nodeIds.length - 1)];
+        };
+
         const _executeRefresh = (paragraph) => {
             const args = paragraph.queryArgs;
 
             agentMonitor.awaitAgent()
                 .then(() => _closeOldQuery(paragraph))
-                .then(() => agentMonitor.query(args.cacheName, args.pageSize, args.query))
+                .then(() => agentMonitor.query(cacheNode(args.cacheName), args.cacheName, args.query, false, args.pageSize))
                 .then(_processQueryResult.bind(this, paragraph))
                 .catch((err) => paragraph.errMsg = err.message);
         };
@@ -1213,7 +1251,7 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
             }
         };
 
-        $scope.execute = function(paragraph) {
+        $scope.execute = (paragraph) => {
             Notebook.save($scope.notebook)
                 .catch(Messages.showError);
 
@@ -1222,16 +1260,16 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
             _showLoading(paragraph, true);
 
             _closeOldQuery(paragraph)
-                .then(function() {
+                .then(() => {
                     const args = paragraph.queryArgs = {
                         cacheName: paragraph.cacheName,
                         pageSize: paragraph.pageSize,
                         query: paragraph.query
                     };
 
-                    return agentMonitor.query(args.cacheName, args.pageSize, args.query);
+                    return agentMonitor.query(cacheNode(paragraph.cacheName), args.cacheName, args.query, false, args.pageSize);
                 })
-                .then(function(res) {
+                .then((res) => {
                     _processQueryResult(paragraph, res);
 
                     _tryStartRefresh(paragraph);
@@ -1250,7 +1288,7 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
             return LegacyUtils.isDefined(paragraph.queryArgs);
         };
 
-        const _cancelRefresh = function(paragraph) {
+        const _cancelRefresh = (paragraph) => {
             if (paragraph.rate && paragraph.rate.stopTime) {
                 delete paragraph.queryArgs;
 
@@ -1262,7 +1300,7 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
             }
         };
 
-        $scope.explain = function(paragraph) {
+        $scope.explain = (paragraph) => {
             Notebook.save($scope.notebook)
                 .catch(Messages.showError);
 
@@ -1271,14 +1309,14 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
             _showLoading(paragraph, true);
 
             _closeOldQuery(paragraph)
-                .then(function() {
+                .then(() => {
                     const args = paragraph.queryArgs = {
                         cacheName: paragraph.cacheName,
                         pageSize: paragraph.pageSize,
                         query: 'EXPLAIN ' + paragraph.query
                     };
 
-                    return agentMonitor.query(args.cacheName, args.pageSize, args.query);
+                    return agentMonitor.query(cacheNode(paragraph.cacheName), args.cacheName, args.query, false, args.pageSize);
                 })
                 .then(_processQueryResult.bind(this, paragraph))
                 .catch((err) => {
@@ -1289,7 +1327,7 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
                 .then(() => paragraph.ace.focus());
         };
 
-        $scope.scan = function(paragraph) {
+        $scope.scan = (paragraph, query = null) => {
             Notebook.save($scope.notebook)
                 .catch(Messages.showError);
 
@@ -1301,10 +1339,11 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
                 .then(() => {
                     const args = paragraph.queryArgs = {
                         cacheName: paragraph.cacheName,
-                        pageSize: paragraph.pageSize
+                        pageSize: paragraph.pageSize,
+                        query
                     };
 
-                    return agentMonitor.query(args.cacheName, args.pageSize);
+                    return agentMonitor.query(cacheNode(paragraph.cacheName), args.cacheName, query, false, args.pageSize);
                 })
                 .then(_processQueryResult.bind(this, paragraph))
                 .catch((err) => {
@@ -1315,6 +1354,15 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
                 .then(() => paragraph.ace.focus());
         };
 
+        $scope.scanWithFilter = (paragraph) => {
+            ScanFilterInput.open()
+                .then(({filter, caseSensitive}) => {
+                    const prefix = caseSensitive ? SCAN_CACHE_WITH_FILTER_CASE_SENSITIVE : SCAN_CACHE_WITH_FILTER;
+
+                    $scope.scan(paragraph, `${prefix}${filter}`);
+                });
+        };
+
         function _updatePieChartsWithData(paragraph, newDatum) {
             $timeout(() => {
                 _.forEach(paragraph.charts, function(chart) {
@@ -1332,18 +1380,20 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
             });
         }
 
-        $scope.nextPage = function(paragraph) {
+        $scope.nextPage = (paragraph) => {
             _showLoading(paragraph, true);
 
             paragraph.queryArgs.pageSize = paragraph.pageSize;
 
-            agentMonitor.next(paragraph.queryId, paragraph.pageSize)
-                .then(function(res) {
+            agentMonitor.next(paragraph.resNodeId, paragraph.queryId, paragraph.pageSize)
+                .then((res) => {
                     paragraph.page++;
 
                     paragraph.total += paragraph.rows.length;
 
-                    paragraph.rows = res.items;
+                    paragraph.duration = res.duration;
+
+                    paragraph.rows = res.rows;
 
                     if (paragraph.chart()) {
                         if (paragraph.result === 'pie')
@@ -1352,11 +1402,11 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
                             _updateChartsWithData(paragraph, _chartDatum(paragraph));
                     }
 
-                    paragraph.gridOptions.updateRows(paragraph.rows);
+                    paragraph.gridOptions.adjustHeight(paragraph.rows.length);
 
                     _showLoading(paragraph, false);
 
-                    if (res.last)
+                    if (!res.hasMore)
                         delete paragraph.queryId;
                 })
                 .catch((err) => {
@@ -1424,8 +1474,8 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
         $scope.exportCsvAll = function(paragraph) {
             const args = paragraph.queryArgs;
 
-            agentMonitor.queryGetAll(args.cacheName, args.query)
-                .then((res) => _export(paragraph.name + '-all.csv', paragraph.columnFilter, res.fieldsMetadata, res.items))
+            agentMonitor.queryGetAll(cacheNode(args.cacheName), args.cacheName, args.query, false)
+                .then((res) => _export(paragraph.name + '-all.csv', paragraph.columnFilter, res.columns, res.rows))
                 .catch(Messages.showError)
                 .then(() => paragraph.ace.focus());
         };
@@ -1544,6 +1594,18 @@ export default ['$rootScope', '$scope', '$http', '$q', '$timeout', '$interval',
                     scope.title = 'SCAN query';
                     scope.content = [`SCAN query for cache: <b>${$scope.maskCacheName(paragraph.queryArgs.cacheName)}</b>`];
                 }
+                if (paragraph.queryArgs.query.startsWith(SCAN_CACHE_WITH_FILTER)) {
+                    scope.title = 'SCAN query';
+
+                    let filter = '';
+
+                    if (paragraph.queryArgs.query.startsWith(SCAN_CACHE_WITH_FILTER_CASE_SENSITIVE))
+                        filter = paragraph.queryArgs.query.substr(SCAN_CACHE_WITH_FILTER_CASE_SENSITIVE.length);
+                    else
+                        filter = paragraph.queryArgs.query.substr(SCAN_CACHE_WITH_FILTER.length);
+
+                    scope.content = [`SCAN query for cache: <b>${$scope.maskCacheName(paragraph.queryArgs.cacheName)}</b> with filter: <b>${filter}</b>`];
+                }
                 else if (paragraph.queryArgs.query .startsWith('EXPLAIN ')) {
                     scope.title = 'Explain query';
                     scope.content = [paragraph.queryArgs.query];

http://git-wip-us.apache.org/repos/asf/ignite/blob/0a1fa244/modules/web-console/frontend/app/modules/sql/sql.module.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/sql/sql.module.js b/modules/web-console/frontend/app/modules/sql/sql.module.js
index 703ecc8..daf5cce 100644
--- a/modules/web-console/frontend/app/modules/sql/sql.module.js
+++ b/modules/web-console/frontend/app/modules/sql/sql.module.js
@@ -19,6 +19,7 @@ import angular from 'angular';
 
 import NotebookData from './Notebook.data';
 import Notebook from './Notebook.service';
+import ScanFilterInput from './scan-filter-input.service';
 import notebook from './notebook.controller';
 import sql from './sql.controller';
 
@@ -52,5 +53,6 @@ angular.module('ignite-console.sql', [
     )
     .service('IgniteNotebookData', NotebookData)
     .service('IgniteNotebook', Notebook)
+    .service('IgniteScanFilterInput', ScanFilterInput)
     .controller('notebookController', notebook)
     .controller('sqlController', sql);

http://git-wip-us.apache.org/repos/asf/ignite/blob/0a1fa244/modules/web-console/frontend/gulpfile.babel.js/paths.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/gulpfile.babel.js/paths.js b/modules/web-console/frontend/gulpfile.babel.js/paths.js
index 4441cfa..9134e44 100644
--- a/modules/web-console/frontend/gulpfile.babel.js/paths.js
+++ b/modules/web-console/frontend/gulpfile.babel.js/paths.js
@@ -28,7 +28,8 @@ const jadePaths = [
     './views/*.jade',
     './views/**/*.jade',
     './app/helpers/**/*.jade',
-    './app/modules/states/configuration/**/*.jade'
+    './app/modules/states/configuration/**/*.jade',
+    './app/modules/sql/*.jade'
 ];
 
 const resourcePaths = [

http://git-wip-us.apache.org/repos/asf/ignite/blob/0a1fa244/modules/web-console/frontend/public/stylesheets/style.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/public/stylesheets/style.scss b/modules/web-console/frontend/public/stylesheets/style.scss
index 3704643..994595a 100644
--- a/modules/web-console/frontend/public/stylesheets/style.scss
+++ b/modules/web-console/frontend/public/stylesheets/style.scss
@@ -641,29 +641,6 @@ button.form-control {
         }
     }
 
-    .sql-controls {
-        margin: 10px 0;
-        padding: 0 10px;
-    }
-
-    .sql-table-total {
-        padding: 0 10px;
-
-        label, b {
-            display: inline-block;
-
-            padding-top: 5px;
-
-            height: 27px;
-        }
-
-        margin-bottom: 10px;
-    }
-
-    .sql-table {
-        height: 400px;
-    }
-
     table thead {
         background-color: white;
     }
@@ -682,38 +659,45 @@ button.form-control {
         line-height: 55px;
     }
 
-    .sql-error-result {
-        padding: 10px 0;
+    .sql-controls {
+        border-top: 1px solid $ignite-border-color;
 
-        text-align: center;
-        color: $brand-primary;
+        padding: 10px 10px;
+    }
 
+    .sql-result {
         border-top: 1px solid $ignite-border-color;
-    }
 
-    .sql-empty-result {
-        margin-top: 10px;
-        margin-bottom: 10px;
-        text-align: center;
-        color: $ignite-placeholder-color;
-    }
+        .error {
+            padding: 10px 10px;
 
-    .sql-next {
-        float: right;
+            text-align: center;
+            color: $brand-primary;
+        }
 
-        .disabled {
-            cursor: default;
-            text-decoration: none;
+        .empty {
+            padding: 10px 10px;
+
+            text-align: center;
+            color: $ignite-placeholder-color;
         }
 
-        a {
-            margin-right: 5px;
-            margin-bottom: 5px;
+        .total {
+            padding: 10px 10px;
         }
 
-        i {
-            margin-top: 3px;
-            margin-right: 10px;
+        .table {
+            margin: 0
+        }
+
+        .chart {
+            margin: 0
+        }
+
+        .footer {
+            border-top: 1px solid $ignite-border-color;
+
+            padding: 5px 10px;
         }
     }
 }

http://git-wip-us.apache.org/repos/asf/ignite/blob/0a1fa244/modules/web-console/frontend/views/sql/sql.jade
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/views/sql/sql.jade b/modules/web-console/frontend/views/sql/sql.jade
index 582c8ca..570517d 100644
--- a/modules/web-console/frontend/views/sql/sql.jade
+++ b/modules/web-console/frontend/views/sql/sql.jade
@@ -28,8 +28,8 @@ mixin result-toolbar
         +btn-toolbar-data('fa-line-chart', 'line', 'Show line chart<br/>By default first column - X values, second column - Y values<br/>In case of one column it will be treated as Y values')
         +btn-toolbar-data('fa-area-chart', 'area', 'Show area chart<br/>By default first column - X values, second column - Y values<br/>In case of one column it will be treated as Y values')
 
-mixin chart-settings(mdl)
-    .row
+mixin chart-settings
+    .total.row
         .col-xs-4
             .chart-settings-link(ng-show='paragraph.chart && paragraph.chartColumns.length > 0')
                 a(title='Click to show chart settings dialog' ng-click='$event.stopPropagation()' bs-popover data-template-url='/sql/chart-settings.html' data-placement='bottom' data-auto-close='1' data-trigger='click')
@@ -86,9 +86,13 @@ mixin query-controls
     .sql-controls
         a.btn.btn-primary(ng-disabled='!actionAvailable(paragraph, true)' ng-click='actionAvailable(paragraph, true) && execute(paragraph)' data-placement='bottom' bs-tooltip data-title='{{actionTooltip(paragraph, "execute", true)}}') Execute
         a.btn.btn-primary(ng-disabled='!actionAvailable(paragraph, true)' ng-click='actionAvailable(paragraph, true) && explain(paragraph)' data-placement='bottom' bs-tooltip data-title='{{actionTooltip(paragraph, "explain", true)}}') Explain
-        a.btn.btn-primary(ng-disabled='!actionAvailable(paragraph, false)' ng-click='actionAvailable(paragraph, false) && scan(paragraph)' data-placement='bottom' bs-tooltip data-title='{{actionTooltip(paragraph, "execute scan", false)}}') Scan
+        .btn-group(ng-disabled='!actionAvailable(paragraph, false)')
+            a.btn.btn-primary.fieldButton(ng-click='actionAvailable(paragraph, false) && scan(paragraph)' data-placement='bottom' bs-tooltip data-title='{{actionTooltip(paragraph, "execute scan", false)}}') Scan
+            a.btn.btn-primary(data-toggle='dropdown' data-container='body' bs-dropdown='[{ text: "Scan with filter", click: "actionAvailable(paragraph, false) && scanWithFilter(paragraph)" }]' data-placement='bottom-right')
+                span.caret
+
         .pull-right
-            labelHide System columns:
+            label.tipLabel System columns:
             a.btn.btn-default.fa.fa-bars.tipLabel(ng-class='{"btn-info": paragraph.systemColumns}' ng-click='toggleSystemColumns(paragraph)' ng-disabled='paragraph.disabledSystemColumns' bs-tooltip data-title='Show "_KEY", "_VAL" columns')
             label.tipLabel Refresh rate:
             button.btn.btn-default.fa.fa-clock-o.tipLabel(title='Click to show refresh rate dialog' ng-class='{"btn-info": paragraph.rate && paragraph.rate.installed}' bs-popover data-template-url='/sql/paragraph-rate.html' data-placement='left' data-auto-close='1' data-trigger='click') {{rateAsString(paragraph)}}
@@ -96,10 +100,11 @@ mixin query-controls
             button.select-toggle.fieldButton.btn.btn-default(ng-model='paragraph.pageSize' bs-options='item for item in pageSizes' bs-select bs-tooltip data-placement='bottom-right' data-title='Max number of rows to show in query result as one page')
 
 mixin table-result
-    .sql-table-total.row
+    .total.row
         .col-xs-4
-            label(style='margin-right: 10px;') Page: #[b {{paragraph.page}}]
-            label Results so far: #[b {{paragraph.rows.length + paragraph.total}}]
+            label Page: #[b {{paragraph.page}}]
+            label.margin-left-dflt Results so far: #[b {{paragraph.rows.length + paragraph.total}}]
+            label.margin-left-dflt Duration: #[b {{paragraph.duration | duration}}]
         .col-xs-4
             +result-toolbar
         .col-xs-4
@@ -112,31 +117,19 @@ mixin table-result
 mixin chart-result
     div(ng-show='paragraph.queryExecuted()')
         +chart-settings
-        div(ng-show='paragraph.chartColumns.length > 0 && !paragraph.chartColumnsConfigured()')
-            .sql-empty-result Cannot display chart. Please configure axis using #[b Chart settings]
-        div(ng-show='paragraph.chartColumns.length == 0')
-            .sql-empty-result Cannot display chart. Result set must contain Java build-in type columns. Please change query and execute it again.
+        .empty(ng-show='paragraph.chartColumns.length > 0 && !paragraph.chartColumnsConfigured()') Cannot display chart. Please configure axis using #[b Chart settings]
+        .empty(ng-show='paragraph.chartColumns.length == 0') Cannot display chart. Result set must contain Java build-in type columns. Please change query and execute it again.
         div(ng-show='paragraph.chartColumnsConfigured()')
             div(ng-show='paragraph.timeLineSupported() || !paragraph.chartTimeLineEnabled()')
                 div(ng-repeat='chart in paragraph.charts')
                     nvd3(options='chart.options' data='chart.data' api='chart.api')
-            .sql-empty-result(ng-show='!paragraph.timeLineSupported() && paragraph.chartTimeLineEnabled()') Pie chart does not support 'TIME_LINE' column for X-axis. Please use another column for X-axis or switch to another chart.
-    .sql-empty-result(ng-hide='paragraph.queryExecuted()')
+            .empty(ng-show='!paragraph.timeLineSupported() && paragraph.chartTimeLineEnabled()') Pie chart does not support 'TIME_LINE' column for X-axis. Please use another column for X-axis or switch to another chart.
+    .empty(ng-hide='paragraph.queryExecuted()')
         .row
             .col-xs-4.col-xs-offset-4
                 +result-toolbar
         label.margin-top-dflt Charts do not support #[b Explain] and #[b Scan] query
 
-mixin footer-controls
-    hr(style='margin-top: 0; margin-bottom: 5px')
-    a(style='float: left; margin-left: 10px; margin-bottom: 5px' ng-click='showResultQuery(paragraph)') Show query
-
-    -var nextVisibleCondition = 'paragraph.queryId && (paragraph.table() || paragraph.chart() && (paragraph.timeLineSupported() || !paragraph.chartTimeLineEnabled()))'
-
-    .sql-next(ng-show=nextVisibleCondition)
-        i.fa.fa-chevron-circle-right(ng-class='{disabled: paragraph.loading}' ng-click='!paragraph.loading && nextPage(paragraph)')
-        a(ng-class='{disabled: paragraph.loading}' ng-click='!paragraph.loading && nextPage(paragraph)') Next
-
 .row(ng-controller='sqlController')
     .docs-content
         .row(ng-if='notebook' bs-affix style='margin-bottom: 20px;')
@@ -182,20 +175,19 @@ mixin footer-controls
                                     .empty-caches(ng-show='caches.length == 0')
                                         label No caches
                             .col-sm-12
-                                hr(style='margin: 0')
-                            .col-sm-12
                                 +query-controls
-                            .col-sm-12.sql-error-result(ng-show='paragraph.errMsg') Error: {{paragraph.errMsg}}
-                            .col-sm-12(ng-show='!paragraph.errMsg && paragraph.queryArgs')
-                                hr(style='margin-top: 0; margin-bottom: 10px')
-
-                                .sql-empty-result(ng-show='!paragraph.nonEmpty()') Result set is empty
-
-                                div(ng-show='paragraph.table() && paragraph.nonEmpty()')
+                            .col-sm-12.sql-result(ng-switch='paragraph.resultType()')
+                                .error(ng-switch-when='error') Error: {{paragraph.errMsg}}
+                                .empty(ng-switch-when='empty') Result set is empty
+                                .table(ng-switch-when='table')
                                     +table-result
-
-                                div(ng-show='paragraph.chart() && paragraph.nonEmpty()')
+                                .chart(ng-switch-when='chart')
                                     +chart-result
+                                .footer.clearfix(ng-show='paragraph.nonRefresh()')
+                                    a.pull-left(ng-click='showResultQuery(paragraph)') Show query
+
+                                    -var nextVisibleCondition = 'paragraph.queryId && (paragraph.table() || paragraph.chart() && (paragraph.timeLineSupported() || !paragraph.chartTimeLineEnabled()))'
 
-                                div(ng-show='!paragraph.refreshExecuting()')
-                                    +footer-controls
+                                    .pull-right(ng-show=nextVisibleCondition ng-class='{disabled: paragraph.loading}' ng-click='!paragraph.loading && nextPage(paragraph)')
+                                        i.fa.fa-chevron-circle-right
+                                        a Next