http://git-wip-us.apache.org/repos/asf/ignite/blob/e1b8686a/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/controller.ts
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/controller.ts
 
b/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/controller.ts
new file mode 100644
index 0000000..3ac3177
--- /dev/null
+++ 
b/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/controller.ts
@@ -0,0 +1,2007 @@
+/*
+ * 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 _ from 'lodash';
+import {nonEmpty, nonNil} from 'app/utils/lodashMixins';
+import id8 from 'app/utils/id8';
+import 'rxjs/add/operator/mergeMap';
+import 'rxjs/add/operator/merge';
+import 'rxjs/add/operator/switchMap';
+import 'rxjs/add/operator/exhaustMap';
+import 'rxjs/add/operator/distinctUntilChanged';
+
+import { fromPromise } from 'rxjs/observable/fromPromise';
+import { timer } from 'rxjs/observable/timer';
+import { defer } from 'rxjs/observable/defer';
+
+import {CSV} from 'app/services/CSV';
+
+import paragraphRateTemplateUrl from 'views/sql/paragraph-rate.tpl.pug';
+import cacheMetadataTemplateUrl from 'views/sql/cache-metadata.tpl.pug';
+import chartSettingsTemplateUrl from 'views/sql/chart-settings.tpl.pug';
+import messageTemplateUrl from 'views/templates/message.tpl.pug';
+
+import {default as Notebook} from '../../notebook.service';
+import {default as MessagesServiceFactory} from 
'app/services/Messages.service';
+import {default as LegacyConfirmServiceFactory} from 
'app/services/Confirm.service';
+import {default as InputDialog} from 
'app/components/input-dialog/input-dialog.service';
+import {QueryActions} from './components/query-actions-button/controller';
+
+// 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 NON_COLLOCATED_JOINS_SINCE = '1.7.0';
+
+const COLLOCATED_QUERY_SINCE = [['2.3.5', '2.4.0'], ['2.4.6', '2.5.0'], 
['2.5.1-p13', '2.6.0'], '2.7.0'];
+
+const ENFORCE_JOIN_SINCE = [['1.7.9', '1.8.0'], ['1.8.4', '1.9.0'], '1.9.1'];
+
+const LAZY_QUERY_SINCE = [['2.1.4-p1', '2.2.0'], '2.2.1'];
+
+const DDL_SINCE = [['2.1.6', '2.2.0'], '2.3.0'];
+
+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('.');
+};
+
+let paragraphId = 0;
+
+class Paragraph {
+    name: string;
+    qryType: 'scan' | 'query';
+
+    constructor($animate, $timeout, JavaTypes, errorParser, paragraph) {
+        const self = this;
+
+        self.id = 'paragraph-' + paragraphId++;
+        self.qryType = paragraph.qryType || 'query';
+        self.maxPages = 0;
+        self.filter = '';
+        self.useAsDefaultSchema = false;
+        self.localQueryMode = false;
+        self.csvIsPreparing = false;
+        self.scanningInProgress = false;
+
+        _.assign(this, paragraph);
+
+        Object.defineProperty(this, 'gridOptions', {value: {
+            enableGridMenu: false,
+            enableColumnMenus: false,
+            flatEntityAccess: true,
+            fastWatch: true,
+            categories: [],
+            rebuildColumns() {
+                if (_.isNil(this.api))
+                    return;
+
+                this.categories.length = 0;
+
+                this.columnDefs = _.reduce(self.meta, (cols, col, idx) => {
+                    cols.push({
+                        displayName: col.fieldName,
+                        headerTooltip: _fullColName(col),
+                        field: idx.toString(),
+                        minWidth: 50,
+                        cellClass: 'cell-left',
+                        visible: self.columnFilter(col)
+                    });
+
+                    this.categories.push({
+                        name: col.fieldName,
+                        visible: self.columnFilter(col),
+                        enableHiding: true
+                    });
+
+                    return cols;
+                }, []);
+
+                $timeout(() => this.api.core.notifyDataChange('column'));
+            },
+            adjustHeight() {
+                if (_.isNil(this.api))
+                    return;
+
+                this.data = self.rows;
+
+                const height = Math.min(self.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;
+
+                this.rebuildColumns();
+
+                this.adjustHeight();
+            }
+        }});
+
+        Object.defineProperty(this, 'chartHistory', {value: []});
+
+        Object.defineProperty(this, 'error', {value: {
+            root: {},
+            message: ''
+        }});
+
+        this.setError = (err) => {
+            this.error.root = err;
+            this.error.message = errorParser.extractMessage(err);
+
+            let cause = err;
+
+            while (nonNil(cause)) {
+                if (nonEmpty(cause.className) &&
+                    _.includes(['SQLException', 'JdbcSQLException', 
'QueryCancelledException'], JavaTypes.shortClassName(cause.className))) {
+                    this.error.message = 
errorParser.extractMessage(cause.message || cause.className);
+
+                    break;
+                }
+
+                cause = cause.cause;
+            }
+
+            if (_.isEmpty(this.error.message) && nonEmpty(err.className)) {
+                this.error.message = 'Internal cluster error';
+
+                if (nonEmpty(err.className))
+                    this.error.message += ': ' + err.className;
+            }
+        };
+    }
+
+    resultType() {
+        if (_.isNil(this.queryArgs))
+            return null;
+
+        if (nonEmpty(this.error.message))
+            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 nonEmpty(this.meta) || nonEmpty(this.error.message);
+    }
+
+    scanExplain() {
+        return this.queryExecuted() && this.queryArgs.type !== 'QUERY';
+    }
+
+    timeLineSupported() {
+        return this.result !== 'pie';
+    }
+
+    chartColumnsConfigured() {
+        return nonEmpty(this.chartKeyCols) && nonEmpty(this.chartValCols);
+    }
+
+    chartTimeLineEnabled() {
+        return nonEmpty(this.chartKeyCols) && _.eq(this.chartKeyCols[0], 
TIME_LINE);
+    }
+
+    executionInProgress(showLocal = false) {
+        return this.loading && (this.localQueryMode === showLocal);
+    }
+
+    checkScanInProgress(showLocal = false) {
+        return this.scanningInProgress && (this.localQueryMode === showLocal);
+    }
+
+    cancelRefresh($interval) {
+        if (this.rate && this.rate.stopTime) {
+            $interval.cancel(this.rate.stopTime);
+
+            delete this.rate.stopTime;
+        }
+    }
+
+    reset($interval) {
+        this.meta = [];
+        this.chartColumns = [];
+        this.chartKeyCols = [];
+        this.chartValCols = [];
+        this.error.root = {};
+        this.error.message = '';
+        this.rows = [];
+        this.duration = 0;
+
+        this.cancelRefresh($interval);
+    }
+}
+
+// Controller for SQL notebook screen.
+export class NotebookCtrl {
+    static $inject = ['IgniteInput', '$rootScope', '$scope', '$http', '$q', 
'$timeout', '$interval', '$animate', '$location', '$anchorScroll', '$state', 
'$filter', '$modal', '$popover', 'IgniteLoading', 'IgniteLegacyUtils', 
'IgniteMessages', 'IgniteConfirm', 'AgentManager', 'IgniteChartColors', 
'IgniteNotebook', 'IgniteNodes', 'uiGridExporterConstants', 'IgniteVersion', 
'IgniteActivitiesData', 'JavaTypes', 'IgniteCopyToClipboard', 'CSV', 
'IgniteErrorParser', 'DemoInfo'];
+
+    /**
+     * @param {CSV} CSV
+     */
+    constructor(private IgniteInput: InputDialog, $root, private $scope, 
$http, $q, $timeout, $interval, $animate, $location, $anchorScroll, $state, 
$filter, $modal, $popover, Loading, LegacyUtils, private Messages: 
ReturnType<typeof MessagesServiceFactory>, private Confirm: ReturnType<typeof 
LegacyConfirmServiceFactory>, agentMgr, IgniteChartColors, private Notebook: 
Notebook, Nodes, uiGridExporterConstants, Version, ActivitiesData, JavaTypes, 
IgniteCopyToClipboard, CSV, errorParser, DemoInfo) {
+        const $ctrl = this;
+
+        this.CSV = CSV;
+        Object.assign(this, { $root, $scope, $http, $q, $timeout, $interval, 
$animate, $location, $anchorScroll, $state, $filter, $modal, $popover, Loading, 
LegacyUtils, Messages, Confirm, agentMgr, IgniteChartColors, Notebook, Nodes, 
uiGridExporterConstants, Version, ActivitiesData, JavaTypes, errorParser, 
DemoInfo });
+
+        // Define template urls.
+        $ctrl.paragraphRateTemplateUrl = paragraphRateTemplateUrl;
+        $ctrl.cacheMetadataTemplateUrl = cacheMetadataTemplateUrl;
+        $ctrl.chartSettingsTemplateUrl = chartSettingsTemplateUrl;
+        $ctrl.demoStarted = false;
+
+        this.isDemo = $root.IgniteDemoMode;
+
+        const _tryStopRefresh = function(paragraph) {
+            paragraph.cancelRefresh($interval);
+        };
+
+        const _stopTopologyRefresh = () => {
+            if ($scope.notebook && $scope.notebook.paragraphs)
+                $scope.notebook.paragraphs.forEach((paragraph) => 
_tryStopRefresh(paragraph));
+        };
+
+        $scope.$on('$stateChangeStart', _stopTopologyRefresh);
+
+        $scope.caches = [];
+
+        $scope.pageSizesOptions = [
+            {value: 50, label: '50'},
+            {value: 100, label: '100'},
+            {value: 200, label: '200'},
+            {value: 400, label: '400'},
+            {value: 800, label: '800'},
+            {value: 1000, label: '1000'}
+        ];
+
+        $scope.maxPages = [
+            {label: 'Unlimited', value: 0},
+            {label: '1', value: 1},
+            {label: '5', value: 5},
+            {label: '10', value: 10},
+            {label: '20', value: 20},
+            {label: '50', value: 50},
+            {label: '100', value: 100}
+        ];
+
+        $scope.timeLineSpans = ['1', '5', '10', '15', '30'];
+
+        $scope.aggregateFxs = ['FIRST', 'LAST', 'MIN', 'MAX', 'SUM', 'AVG', 
'COUNT'];
+
+        $scope.modes = LegacyUtils.mkOptions(['PARTITIONED', 'REPLICATED', 
'LOCAL']);
+
+        $scope.loadingText = $root.IgniteDemoMode ? 'Demo grid is starting. 
Please wait...' : 'Loading query notebook screen...';
+
+        $scope.timeUnit = [
+            {value: 1000, label: 'seconds', short: 's'},
+            {value: 60000, label: 'minutes', short: 'm'},
+            {value: 3600000, label: 'hours', short: 'h'}
+        ];
+
+        $scope.metadata = [];
+
+        $scope.metaFilter = '';
+
+        $scope.metaOptions = {
+            nodeChildren: 'children',
+            dirSelectable: true,
+            injectClasses: {
+                iExpanded: 'fa fa-minus-square-o',
+                iCollapsed: 'fa fa-plus-square-o'
+            }
+        };
+
+        const maskCacheName = $filter('defaultName');
+
+        // We need max 1800 items to hold history for 30 mins in case of 
refresh every second.
+        const HISTORY_LENGTH = 1800;
+
+        const MAX_VAL_COLS = IgniteChartColors.length;
+
+        $anchorScroll.yOffset = 55;
+
+        $scope.chartColor = function(index) {
+            return {color: 'white', 'background-color': 
IgniteChartColors[index]};
+        };
+
+        function _chartNumber(arr, idx, dflt) {
+            if (idx >= 0 && arr && arr.length > idx && _.isNumber(arr[idx]))
+                return arr[idx];
+
+            return dflt;
+        }
+
+        function _min(rows, idx, dflt) {
+            let min = _chartNumber(rows[0], idx, dflt);
+
+            _.forEach(rows, (row) => {
+                const v = _chartNumber(row, idx, dflt);
+
+                if (v < min)
+                    min = v;
+            });
+
+            return min;
+        }
+
+        function _max(rows, idx, dflt) {
+            let max = _chartNumber(rows[0], idx, dflt);
+
+            _.forEach(rows, (row) => {
+                const v = _chartNumber(row, idx, dflt);
+
+                if (v > max)
+                    max = v;
+            });
+
+            return max;
+        }
+
+        function _sum(rows, idx) {
+            let sum = 0;
+
+            _.forEach(rows, (row) => sum += _chartNumber(row, idx, 0));
+
+            return sum;
+        }
+
+        function _aggregate(rows, aggFx, idx, dflt) {
+            const len = rows.length;
+
+            switch (aggFx) {
+                case 'FIRST':
+                    return _chartNumber(rows[0], idx, dflt);
+
+                case 'LAST':
+                    return _chartNumber(rows[len - 1], idx, dflt);
+
+                case 'MIN':
+                    return _min(rows, idx, dflt);
+
+                case 'MAX':
+                    return _max(rows, idx, dflt);
+
+                case 'SUM':
+                    return _sum(rows, idx);
+
+                case 'AVG':
+                    return len > 0 ? _sum(rows, idx) / len : 0;
+
+                case 'COUNT':
+                    return len;
+
+                default:
+            }
+
+            return 0;
+        }
+
+        function _chartLabel(arr, idx, dflt) {
+            if (arr && arr.length > idx && _.isString(arr[idx]))
+                return arr[idx];
+
+            return dflt;
+        }
+
+        function _chartDatum(paragraph) {
+            let datum = [];
+
+            if (paragraph.chartColumnsConfigured()) {
+                paragraph.chartValCols.forEach(function(valCol) {
+                    let index = 0;
+                    let values = [];
+                    const colIdx = valCol.value;
+
+                    if (paragraph.chartTimeLineEnabled()) {
+                        const aggFx = valCol.aggFx;
+                        const colLbl = valCol.label + ' [' + aggFx + ']';
+
+                        if (paragraph.charts && paragraph.charts.length === 1)
+                            datum = paragraph.charts[0].data;
+
+                        const chartData = _.find(datum, {series: 
valCol.label});
+
+                        const leftBound = new Date();
+                        leftBound.setMinutes(leftBound.getMinutes() - 
parseInt(paragraph.timeLineSpan, 10));
+
+                        if (chartData) {
+                            const lastItem = _.last(paragraph.chartHistory);
+
+                            values = chartData.values;
+
+                            values.push({
+                                x: lastItem.tm,
+                                y: _aggregate(lastItem.rows, aggFx, colIdx, 
index++)
+                            });
+
+                            while (values.length > 0 && values[0].x < 
leftBound)
+                                values.shift();
+                        }
+                        else {
+                            _.forEach(paragraph.chartHistory, (history) => {
+                                if (history.tm >= leftBound) {
+                                    values.push({
+                                        x: history.tm,
+                                        y: _aggregate(history.rows, aggFx, 
colIdx, index++)
+                                    });
+                                }
+                            });
+
+                            datum.push({series: valCol.label, key: colLbl, 
values});
+                        }
+                    }
+                    else {
+                        index = paragraph.total;
+
+                        values = _.map(paragraph.rows, function(row) {
+                            const xCol = paragraph.chartKeyCols[0].value;
+
+                            const v = {
+                                x: _chartNumber(row, xCol, index),
+                                xLbl: _chartLabel(row, xCol, null),
+                                y: _chartNumber(row, colIdx, index)
+                            };
+
+                            index++;
+
+                            return v;
+                        });
+
+                        datum.push({series: valCol.label, key: valCol.label, 
values});
+                    }
+                });
+            }
+
+            return datum;
+        }
+
+        function _xX(d) {
+            return d.x;
+        }
+
+        function _yY(d) {
+            return d.y;
+        }
+
+        function _xAxisTimeFormat(d) {
+            return d3.time.format('%X')(new Date(d));
+        }
+
+        const _intClasses = ['java.lang.Byte', 'java.lang.Integer', 
'java.lang.Long', 'java.lang.Short'];
+
+        function _intType(cls) {
+            return _.includes(_intClasses, cls);
+        }
+
+        const _xAxisWithLabelFormat = function(paragraph) {
+            return function(d) {
+                const values = paragraph.charts[0].data[0].values;
+
+                const fmt = _intType(paragraph.chartKeyCols[0].type) ? 'd' : 
',.2f';
+
+                const dx = values[d];
+
+                if (!dx)
+                    return d3.format(fmt)(d);
+
+                const lbl = dx.xLbl;
+
+                return lbl ? lbl : d3.format(fmt)(d);
+            };
+        };
+
+        function _xAxisLabel(paragraph) {
+            return _.isEmpty(paragraph.chartKeyCols) ? 'X' : 
paragraph.chartKeyCols[0].label;
+        }
+
+        const _yAxisFormat = function(d) {
+            const fmt = d < 1000 ? ',.2f' : '.3s';
+
+            return d3.format(fmt)(d);
+        };
+
+        function _updateCharts(paragraph) {
+            $timeout(() => _.forEach(paragraph.charts, (chart) => 
chart.api.update()), 100);
+        }
+
+        function _updateChartsWithData(paragraph, newDatum) {
+            $timeout(() => {
+                if (!paragraph.chartTimeLineEnabled()) {
+                    const chartDatum = paragraph.charts[0].data;
+
+                    chartDatum.length = 0;
+
+                    _.forEach(newDatum, (series) => chartDatum.push(series));
+                }
+
+                paragraph.charts[0].api.update();
+            });
+        }
+
+        function _yAxisLabel(paragraph) {
+            const cols = paragraph.chartValCols;
+
+            const tml = paragraph.chartTimeLineEnabled();
+
+            return _.isEmpty(cols) ? 'Y' : _.map(cols, function(col) {
+                let lbl = col.label;
+
+                if (tml)
+                    lbl += ' [' + col.aggFx + ']';
+
+                return lbl;
+            }).join(', ');
+        }
+
+        function _barChart(paragraph) {
+            const datum = _chartDatum(paragraph);
+
+            if (_.isEmpty(paragraph.charts)) {
+                const stacked = paragraph.chartsOptions && 
paragraph.chartsOptions.barChart
+                    ? paragraph.chartsOptions.barChart.stacked
+                    : true;
+
+                const options = {
+                    chart: {
+                        type: 'multiBarChart',
+                        height: 400,
+                        margin: {left: 70},
+                        duration: 0,
+                        x: _xX,
+                        y: _yY,
+                        xAxis: {
+                            axisLabel: _xAxisLabel(paragraph),
+                            tickFormat: paragraph.chartTimeLineEnabled() ? 
_xAxisTimeFormat : _xAxisWithLabelFormat(paragraph),
+                            showMaxMin: false
+                        },
+                        yAxis: {
+                            axisLabel: _yAxisLabel(paragraph),
+                            tickFormat: _yAxisFormat
+                        },
+                        color: IgniteChartColors,
+                        stacked,
+                        showControls: true,
+                        legend: {
+                            vers: 'furious',
+                            margin: {right: -15}
+                        }
+                    }
+                };
+
+                paragraph.charts = [{options, data: datum}];
+
+                _updateCharts(paragraph);
+            }
+            else
+                _updateChartsWithData(paragraph, datum);
+        }
+
+        function _pieChartDatum(paragraph) {
+            const datum = [];
+
+            if (paragraph.chartColumnsConfigured() && 
!paragraph.chartTimeLineEnabled()) {
+                paragraph.chartValCols.forEach(function(valCol) {
+                    let index = paragraph.total;
+
+                    const values = _.map(paragraph.rows, (row) => {
+                        const xCol = paragraph.chartKeyCols[0].value;
+
+                        const v = {
+                            x: xCol < 0 ? index : row[xCol],
+                            y: _chartNumber(row, valCol.value, index)
+                        };
+
+                        // Workaround for known problem with zero values on 
Pie chart.
+                        if (v.y === 0)
+                            v.y = 0.0001;
+
+                        index++;
+
+                        return v;
+                    });
+
+                    datum.push({series: paragraph.chartKeyCols[0].label, key: 
valCol.label, values});
+                });
+            }
+
+            return datum;
+        }
+
+        function _pieChart(paragraph) {
+            let datum = _pieChartDatum(paragraph);
+
+            if (datum.length === 0)
+                datum = [{values: []}];
+
+            paragraph.charts = _.map(datum, function(data) {
+                return {
+                    options: {
+                        chart: {
+                            type: 'pieChart',
+                            height: 400,
+                            duration: 0,
+                            x: _xX,
+                            y: _yY,
+                            showLabels: true,
+                            labelThreshold: 0.05,
+                            labelType: 'percent',
+                            donut: true,
+                            donutRatio: 0.35,
+                            legend: {
+                                vers: 'furious',
+                                margin: {right: -15}
+                            }
+                        },
+                        title: {
+                            enable: true,
+                            text: data.key
+                        }
+                    },
+                    data: data.values
+                };
+            });
+
+            _updateCharts(paragraph);
+        }
+
+        function _lineChart(paragraph) {
+            const datum = _chartDatum(paragraph);
+
+            if (_.isEmpty(paragraph.charts)) {
+                const options = {
+                    chart: {
+                        type: 'lineChart',
+                        height: 400,
+                        margin: { left: 70 },
+                        duration: 0,
+                        x: _xX,
+                        y: _yY,
+                        xAxis: {
+                            axisLabel: _xAxisLabel(paragraph),
+                            tickFormat: paragraph.chartTimeLineEnabled() ? 
_xAxisTimeFormat : _xAxisWithLabelFormat(paragraph),
+                            showMaxMin: false
+                        },
+                        yAxis: {
+                            axisLabel: _yAxisLabel(paragraph),
+                            tickFormat: _yAxisFormat
+                        },
+                        color: IgniteChartColors,
+                        useInteractiveGuideline: true,
+                        legend: {
+                            vers: 'furious',
+                            margin: {right: -15}
+                        }
+                    }
+                };
+
+                paragraph.charts = [{options, data: datum}];
+
+                _updateCharts(paragraph);
+            }
+            else
+                _updateChartsWithData(paragraph, datum);
+        }
+
+        function _areaChart(paragraph) {
+            const datum = _chartDatum(paragraph);
+
+            if (_.isEmpty(paragraph.charts)) {
+                const style = paragraph.chartsOptions && 
paragraph.chartsOptions.areaChart
+                    ? paragraph.chartsOptions.areaChart.style
+                    : 'stack';
+
+                const options = {
+                    chart: {
+                        type: 'stackedAreaChart',
+                        height: 400,
+                        margin: {left: 70},
+                        duration: 0,
+                        x: _xX,
+                        y: _yY,
+                        xAxis: {
+                            axisLabel: _xAxisLabel(paragraph),
+                            tickFormat: paragraph.chartTimeLineEnabled() ? 
_xAxisTimeFormat : _xAxisWithLabelFormat(paragraph),
+                            showMaxMin: false
+                        },
+                        yAxis: {
+                            axisLabel: _yAxisLabel(paragraph),
+                            tickFormat: _yAxisFormat
+                        },
+                        color: IgniteChartColors,
+                        style,
+                        legend: {
+                            vers: 'furious',
+                            margin: {right: -15}
+                        }
+                    }
+                };
+
+                paragraph.charts = [{options, data: datum}];
+
+                _updateCharts(paragraph);
+            }
+            else
+                _updateChartsWithData(paragraph, datum);
+        }
+
+        function _chartApplySettings(paragraph, resetCharts) {
+            if (resetCharts)
+                paragraph.charts = [];
+
+            if (paragraph.chart() && paragraph.nonEmpty()) {
+                switch (paragraph.result) {
+                    case 'bar':
+                        _barChart(paragraph);
+                        break;
+
+                    case 'pie':
+                        _pieChart(paragraph);
+                        break;
+
+                    case 'line':
+                        _lineChart(paragraph);
+                        break;
+
+                    case 'area':
+                        _areaChart(paragraph);
+                        break;
+
+                    default:
+                }
+            }
+        }
+
+        $scope.chartRemoveKeyColumn = function(paragraph, index) {
+            paragraph.chartKeyCols.splice(index, 1);
+
+            _chartApplySettings(paragraph, true);
+        };
+
+        $scope.chartRemoveValColumn = function(paragraph, index) {
+            paragraph.chartValCols.splice(index, 1);
+
+            _chartApplySettings(paragraph, true);
+        };
+
+        $scope.chartAcceptKeyColumn = function(paragraph, item) {
+            const accepted = _.findIndex(paragraph.chartKeyCols, item) < 0;
+
+            if (accepted) {
+                paragraph.chartKeyCols = [item];
+
+                _chartApplySettings(paragraph, true);
+            }
+
+            return false;
+        };
+
+        const _numberClasses = ['java.math.BigDecimal', 'java.lang.Byte', 
'java.lang.Double',
+            'java.lang.Float', 'java.lang.Integer', 'java.lang.Long', 
'java.lang.Short'];
+
+        const _numberType = function(cls) {
+            return _.includes(_numberClasses, cls);
+        };
+
+        $scope.chartAcceptValColumn = function(paragraph, item) {
+            const valCols = paragraph.chartValCols;
+
+            const accepted = _.findIndex(valCols, item) < 0 && item.value >= 0 
&& _numberType(item.type);
+
+            if (accepted) {
+                if (valCols.length === MAX_VAL_COLS - 1)
+                    valCols.shift();
+
+                valCols.push(item);
+
+                _chartApplySettings(paragraph, true);
+            }
+
+            return false;
+        };
+
+        $scope.scrollParagraphs = [];
+
+        $scope.rebuildScrollParagraphs = function() {
+            $scope.scrollParagraphs = 
$scope.notebook.paragraphs.map(function(paragraph) {
+                return {
+                    text: paragraph.name,
+                    click: 'scrollToParagraph("' + paragraph.id + '")'
+                };
+            });
+        };
+
+        $scope.scrollToParagraph = (id) => {
+            const idx = _.findIndex($scope.notebook.paragraphs, {id});
+
+            if (idx >= 0) {
+                if (!_.includes($scope.notebook.expandedParagraphs, idx))
+                    $scope.notebook.expandedParagraphs = 
$scope.notebook.expandedParagraphs.concat([idx]);
+
+                if ($scope.notebook.paragraphs[idx].ace)
+                    setTimeout(() => 
$scope.notebook.paragraphs[idx].ace.focus());
+            }
+
+            $location.hash(id);
+
+            $anchorScroll();
+        };
+
+        const _hideColumn = (col) => col.fieldName !== '_KEY' && col.fieldName 
!== '_VAL';
+
+        const _allColumn = () => true;
+
+        $scope.aceInit = function(paragraph) {
+            return function(editor) {
+                editor.setAutoScrollEditorIntoView(true);
+                editor.$blockScrolling = Infinity;
+
+                const renderer = editor.renderer;
+
+                renderer.setHighlightGutterLine(false);
+                renderer.setShowPrintMargin(false);
+                renderer.setOption('fontFamily', 'monospace');
+                renderer.setOption('fontSize', '14px');
+                renderer.setOption('minLines', '5');
+                renderer.setOption('maxLines', '15');
+
+                editor.setTheme('ace/theme/chrome');
+
+                Object.defineProperty(paragraph, 'ace', { value: editor });
+            };
+        };
+
+        /**
+         * Update caches list.
+         */
+        const _refreshCaches = () => {
+            return agentMgr.publicCacheNames()
+                .then((cacheNames) => {
+                    $scope.caches = _.sortBy(_.map(cacheNames, (name) => ({
+                        label: maskCacheName(name, true),
+                        value: name
+                    })), (cache) => cache.label.toLowerCase());
+
+                    _.forEach($scope.notebook.paragraphs, (paragraph) => {
+                        if (!_.includes(cacheNames, paragraph.cacheName))
+                            paragraph.cacheName = _.head(cacheNames);
+                    });
+
+                    // Await for demo caches.
+                    if (!$ctrl.demoStarted && $root.IgniteDemoMode && 
nonEmpty(cacheNames)) {
+                        $ctrl.demoStarted = true;
+
+                        Loading.finish('sqlLoading');
+
+                        _.forEach($scope.notebook.paragraphs, (paragraph) => 
$scope.execute(paragraph));
+                    }
+
+                    $scope.$applyAsync();
+                })
+                .catch((err) => Messages.showError(err));
+        };
+
+        const _startWatch = () => {
+            const awaitClusters$ = fromPromise(
+                agentMgr.startClusterWatch('Leave Queries', 'default-state'));
+
+            const finishLoading$ = defer(() => {
+                if (!$root.IgniteDemoMode)
+                    Loading.finish('sqlLoading');
+            }).take(1);
+
+            const refreshCaches = (period) => {
+                return timer(0, period).exhaustMap(() => 
_refreshCaches()).merge(finishLoading$);
+            };
+
+            this.refresh$ = awaitClusters$
+                .mergeMap(() => agentMgr.currentCluster$)
+                .do(() => Loading.start('sqlLoading'))
+                .do(() => {
+                    _.forEach($scope.notebook.paragraphs, (paragraph) => {
+                        paragraph.reset($interval);
+                    });
+                })
+                .switchMap(() => refreshCaches(5000))
+                .subscribe();
+        };
+
+        const _newParagraph = (paragraph) => {
+            return new Paragraph($animate, $timeout, JavaTypes, errorParser, 
paragraph);
+        };
+
+        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, 
(p) => _newParagraph(p));
+
+                if (_.isEmpty($scope.notebook.paragraphs))
+                    $scope.addQuery();
+                else
+                    $scope.rebuildScrollParagraphs();
+            })
+            .then(() => {
+                if ($root.IgniteDemoMode && sessionStorage.showDemoInfo !== 
'true') {
+                    sessionStorage.showDemoInfo = 'true';
+
+                    this.DemoInfo.show().then(_startWatch);
+                } else
+                    _startWatch();
+            })
+            .catch(() => {
+                $scope.notebookLoadFailed = true;
+
+                Loading.finish('sqlLoading');
+            });
+
+        $scope.renameNotebook = (name) => {
+            if (!name)
+                return;
+
+            if ($scope.notebook.name !== name) {
+                const prevName = $scope.notebook.name;
+
+                $scope.notebook.name = name;
+
+                Notebook.save($scope.notebook)
+                    .then(() => $scope.notebook.edit = false)
+                    .catch((err) => {
+                        $scope.notebook.name = prevName;
+
+                        Messages.showError(err);
+                    });
+            }
+            else
+                $scope.notebook.edit = false;
+        };
+
+        $scope.removeNotebook = (notebook) => Notebook.remove(notebook);
+
+        $scope.addParagraph = (paragraph, sz) => {
+            if ($scope.caches && $scope.caches.length > 0)
+                paragraph.cacheName = _.head($scope.caches).value;
+
+            $scope.notebook.paragraphs.push(paragraph);
+
+            $scope.notebook.expandedParagraphs.push(sz);
+
+            $scope.rebuildScrollParagraphs();
+
+            $location.hash(paragraph.id);
+        };
+
+        $scope.addQuery = function() {
+            const sz = $scope.notebook.paragraphs.length;
+
+            ActivitiesData.post({ group: 'sql', action: '/queries/add/query' 
});
+
+            const paragraph = _newParagraph({
+                name: 'Query' + (sz === 0 ? '' : sz),
+                query: '',
+                pageSize: $scope.pageSizesOptions[1].value,
+                timeLineSpan: $scope.timeLineSpans[0],
+                result: 'none',
+                rate: {
+                    value: 1,
+                    unit: 60000,
+                    installed: false
+                },
+                qryType: 'query',
+                lazy: true
+            });
+
+            $scope.addParagraph(paragraph, sz);
+
+            $timeout(() => {
+                $anchorScroll();
+
+                paragraph.ace.focus();
+            });
+        };
+
+        $scope.addScan = function() {
+            const sz = $scope.notebook.paragraphs.length;
+
+            ActivitiesData.post({ group: 'sql', action: '/queries/add/scan' });
+
+            const paragraph = _newParagraph({
+                name: 'Scan' + (sz === 0 ? '' : sz),
+                query: '',
+                pageSize: $scope.pageSizesOptions[1].value,
+                timeLineSpan: $scope.timeLineSpans[0],
+                result: 'none',
+                rate: {
+                    value: 1,
+                    unit: 60000,
+                    installed: false
+                },
+                qryType: 'scan'
+            });
+
+            $scope.addParagraph(paragraph, sz);
+        };
+
+        function _saveChartSettings(paragraph) {
+            if (!_.isEmpty(paragraph.charts)) {
+                const chart = paragraph.charts[0].api.getScope().chart;
+
+                if (!LegacyUtils.isDefined(paragraph.chartsOptions))
+                    paragraph.chartsOptions = {barChart: {stacked: true}, 
areaChart: {style: 'stack'}};
+
+                switch (paragraph.result) {
+                    case 'bar':
+                        paragraph.chartsOptions.barChart.stacked = 
chart.stacked();
+
+                        break;
+
+                    case 'area':
+                        paragraph.chartsOptions.areaChart.style = 
chart.style();
+
+                        break;
+
+                    default:
+                }
+            }
+        }
+
+        $scope.setResult = function(paragraph, new_result) {
+            if (paragraph.result === new_result)
+                return;
+
+            _saveChartSettings(paragraph);
+
+            paragraph.result = new_result;
+
+            if (paragraph.chart())
+                _chartApplySettings(paragraph, true);
+        };
+
+        $scope.resultEq = function(paragraph, result) {
+            return (paragraph.result === result);
+        };
+
+        $scope.paragraphExpanded = function(paragraph) {
+            const paragraph_idx = _.findIndex($scope.notebook.paragraphs, 
function(item) {
+                return paragraph === item;
+            });
+
+            const panel_idx = _.findIndex($scope.notebook.expandedParagraphs, 
function(item) {
+                return paragraph_idx === item;
+            });
+
+            return panel_idx >= 0;
+        };
+
+        const _columnFilter = function(paragraph) {
+            return paragraph.disabledSystemColumns || paragraph.systemColumns 
? _allColumn : _hideColumn;
+        };
+
+        const _notObjectType = function(cls) {
+            return LegacyUtils.isJavaBuiltInClass(cls);
+        };
+
+        function _retainColumns(allCols, curCols, acceptableType, xAxis, 
unwantedCols) {
+            const retainedCols = [];
+
+            const availableCols = xAxis ? allCols : _.filter(allCols, 
function(col) {
+                return col.value >= 0;
+            });
+
+            if (availableCols.length > 0) {
+                curCols.forEach(function(curCol) {
+                    const col = _.find(availableCols, {label: curCol.label});
+
+                    if (col && acceptableType(col.type)) {
+                        col.aggFx = curCol.aggFx;
+
+                        retainedCols.push(col);
+                    }
+                });
+
+                // If nothing was restored, add first acceptable column.
+                if (_.isEmpty(retainedCols)) {
+                    let col;
+
+                    if (unwantedCols)
+                        col = _.find(availableCols, (avCol) => 
!_.find(unwantedCols, {label: avCol.label}) && acceptableType(avCol.type));
+
+                    if (!col)
+                        col = _.find(availableCols, (avCol) => 
acceptableType(avCol.type));
+
+                    if (col)
+                        retainedCols.push(col);
+                }
+            }
+
+            return retainedCols;
+        }
+
+        const _rebuildColumns = function(paragraph) {
+            _.forEach(_.groupBy(paragraph.meta, 'fieldName'), 
function(colsByName, fieldName) {
+                const colsByTypes = _.groupBy(colsByName, 'typeName');
+
+                const needType = _.keys(colsByTypes).length > 1;
+
+                _.forEach(colsByTypes, function(colsByType, typeName) {
+                    _.forEach(colsByType, function(col, ix) {
+                        col.fieldName = (needType && 
!LegacyUtils.isEmptyString(typeName) ? typeName + '.' : '') + fieldName + (ix > 
0 ? ix : '');
+                    });
+                });
+            });
+
+            paragraph.gridOptions.rebuildColumns();
+
+            paragraph.chartColumns = _.reduce(paragraph.meta, (acc, col, idx) 
=> {
+                if (_notObjectType(col.fieldTypeName)) {
+                    acc.push({
+                        label: col.fieldName,
+                        type: col.fieldTypeName,
+                        aggFx: $scope.aggregateFxs[0],
+                        value: idx.toString()
+                    });
+                }
+
+                return acc;
+            }, []);
+
+            if (paragraph.chartColumns.length > 0) {
+                paragraph.chartColumns.push(TIME_LINE);
+                paragraph.chartColumns.push(ROW_IDX);
+            }
+
+            // We could accept onl not object columns for X axis.
+            paragraph.chartKeyCols = _retainColumns(paragraph.chartColumns, 
paragraph.chartKeyCols, _notObjectType, true);
+
+            // We could accept only numeric columns for Y axis.
+            paragraph.chartValCols = _retainColumns(paragraph.chartColumns, 
paragraph.chartValCols, _numberType, false, paragraph.chartKeyCols);
+        };
+
+        $scope.toggleSystemColumns = function(paragraph) {
+            if (paragraph.disabledSystemColumns)
+                return;
+
+            paragraph.columnFilter = _columnFilter(paragraph);
+
+            paragraph.chartColumns = [];
+
+            _rebuildColumns(paragraph);
+        };
+
+        const _showLoading = (paragraph, enable) => paragraph.loading = enable;
+
+        /**
+         * @param {Object} paragraph Query
+         * @param {Boolean} clearChart Flag is need clear chart model.
+         * @param {{columns: Array, rows: Array, responseNodeId: String, 
queryId: int, hasMore: Boolean}} res Query results.
+         * @private
+         */
+        const _processQueryResult = (paragraph, clearChart, res) => {
+            const prevKeyCols = paragraph.chartKeyCols;
+            const prevValCols = paragraph.chartValCols;
+
+            if (!_.eq(paragraph.meta, res.columns)) {
+                paragraph.meta = [];
+
+                paragraph.chartColumns = [];
+
+                if (!LegacyUtils.isDefined(paragraph.chartKeyCols))
+                    paragraph.chartKeyCols = [];
+
+                if (!LegacyUtils.isDefined(paragraph.chartValCols))
+                    paragraph.chartValCols = [];
+
+                if (res.columns.length) {
+                    const _key = _.find(res.columns, {fieldName: '_KEY'});
+                    const _val = _.find(res.columns, {fieldName: '_VAL'});
+
+                    paragraph.disabledSystemColumns = !(_key && _val) ||
+                        (res.columns.length === 2 && _key && _val) ||
+                        (res.columns.length === 1 && (_key || _val));
+                }
+
+                paragraph.columnFilter = _columnFilter(paragraph);
+
+                paragraph.meta = res.columns;
+
+                _rebuildColumns(paragraph);
+            }
+
+            paragraph.page = 1;
+
+            paragraph.total = 0;
+
+            paragraph.duration = res.duration;
+
+            paragraph.queryId = res.hasMore ? res.queryId : null;
+
+            paragraph.resNodeId = res.responseNodeId;
+
+            paragraph.setError({message: ''});
+
+            // Prepare explain results for display in table.
+            if (paragraph.queryArgs.query && 
paragraph.queryArgs.query.startsWith('EXPLAIN') && res.rows) {
+                paragraph.rows = [];
+
+                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.rows;
+
+            paragraph.gridOptions.adjustHeight(paragraph.rows.length);
+
+            const chartHistory = paragraph.chartHistory;
+
+            // Clear history on query change.
+            if (clearChart) {
+                chartHistory.length = 0;
+
+                _.forEach(paragraph.charts, (chart) => chart.data.length = 0);
+            }
+
+            // Add results to history.
+            chartHistory.push({tm: new Date(), rows: paragraph.rows});
+
+            // Keep history size no more than max length.
+            while (chartHistory.length > HISTORY_LENGTH)
+                chartHistory.shift();
+
+            _showLoading(paragraph, false);
+
+            if (_.isNil(paragraph.result) || paragraph.result === 'none' || 
paragraph.scanExplain())
+                paragraph.result = 'table';
+            else if (paragraph.chart()) {
+                let resetCharts = clearChart;
+
+                if (!resetCharts) {
+                    const curKeyCols = paragraph.chartKeyCols;
+                    const curValCols = paragraph.chartValCols;
+
+                    resetCharts = !prevKeyCols || !prevValCols ||
+                        prevKeyCols.length !== curKeyCols.length ||
+                        prevValCols.length !== curValCols.length;
+                }
+
+                _chartApplySettings(paragraph, resetCharts);
+            }
+        };
+
+        const _closeOldQuery = (paragraph) => {
+            const nid = paragraph.resNodeId;
+
+            if (paragraph.queryId && _.find($scope.caches, ({nodes}) => 
_.find(nodes, {nid: nid.toUpperCase()})))
+                return agentMgr.queryClose(nid, paragraph.queryId);
+
+            return $q.when();
+        };
+
+        /**
+         * @param {String} name Cache name.
+         * @param {Array.<String>} nids Cache name.
+         * @return {Promise<Array.<{nid: string, ip: string, version:string, 
gridName: string, os: string, client: boolean}>>}
+         */
+        const cacheNodesModel = (name, nids) => {
+            return agentMgr.topology(true)
+                .then((nodes) =>
+                    _.reduce(nodes, (acc, node) => {
+                        if (_.includes(nids, node.nodeId)) {
+                            acc.push({
+                                nid: node.nodeId.toUpperCase(),
+                                ip: 
_.head(node.attributes['org.apache.ignite.ips'].split(', ')),
+                                version: 
node.attributes['org.apache.ignite.build.ver'],
+                                gridName: 
node.attributes['org.apache.ignite.ignite.name'],
+                                os: `${node.attributes['os.name']} 
${node.attributes['os.arch']} ${node.attributes['os.version']}`,
+                                client: 
node.attributes['org.apache.ignite.cache.client']
+                            });
+                        }
+
+                        return acc;
+                    }, [])
+                );
+        };
+
+        /**
+         * @param {string} name Cache name.
+         * @param {boolean} local Local query.
+         * @return {Promise<string>} Nid
+         */
+        const _chooseNode = (name, local) => {
+            if (_.isEmpty(name))
+                return Promise.resolve(null);
+
+            return agentMgr.cacheNodes(name)
+                .then((nids) => {
+                    if (local) {
+                        return cacheNodesModel(name, nids)
+                            .then((nodes) => Nodes.selectNode(nodes, 
name).catch(() => {}))
+                            .then((selectedNids) => _.head(selectedNids));
+                    }
+
+                    return nids[_.random(0, nids.length - 1)];
+                });
+        };
+
+        const _executeRefresh = (paragraph) => {
+            const args = paragraph.queryArgs;
+
+            agentMgr.awaitCluster()
+                .then(() => _closeOldQuery(paragraph))
+                .then(() => args.localNid || _chooseNode(args.cacheName, 
false))
+                .then((nid) => agentMgr.querySql(nid, args.cacheName, 
args.query, args.nonCollocatedJoins,
+                    args.enforceJoinOrder, false, !!args.localNid, 
args.pageSize, args.lazy, args.collocated))
+                .then((res) => _processQueryResult(paragraph, false, res))
+                .catch((err) => paragraph.setError(err));
+        };
+
+        const _tryStartRefresh = function(paragraph) {
+            _tryStopRefresh(paragraph);
+
+            if (_.get(paragraph, 'rate.installed') && 
paragraph.queryExecuted()) {
+                $scope.chartAcceptKeyColumn(paragraph, TIME_LINE);
+
+                _executeRefresh(paragraph);
+
+                const delay = paragraph.rate.value * paragraph.rate.unit;
+
+                paragraph.rate.stopTime = $interval(_executeRefresh, delay, 0, 
false, paragraph);
+            }
+        };
+
+        const addLimit = (query, limitSize) =>
+            `SELECT * FROM (
+            ${query} 
+            ) LIMIT ${limitSize}`;
+
+        $scope.nonCollocatedJoinsAvailable = () => {
+            return Version.since(this.agentMgr.clusterVersion, 
NON_COLLOCATED_JOINS_SINCE);
+        };
+
+        $scope.collocatedJoinsAvailable = () => {
+            return Version.since(this.agentMgr.clusterVersion, 
...COLLOCATED_QUERY_SINCE);
+        };
+
+        $scope.enforceJoinOrderAvailable = () => {
+            return Version.since(this.agentMgr.clusterVersion, 
...ENFORCE_JOIN_SINCE);
+        };
+
+        $scope.lazyQueryAvailable = () => {
+            return Version.since(this.agentMgr.clusterVersion, 
...LAZY_QUERY_SINCE);
+        };
+
+        $scope.ddlAvailable = () => {
+            return Version.since(this.agentMgr.clusterVersion, ...DDL_SINCE);
+        };
+
+        $scope.cacheNameForSql = (paragraph) => {
+            return $scope.ddlAvailable() && !paragraph.useAsDefaultSchema ? 
null : paragraph.cacheName;
+        };
+
+        $scope.execute = (paragraph, local = false) => {
+            const nonCollocatedJoins = !!paragraph.nonCollocatedJoins;
+            const enforceJoinOrder = !!paragraph.enforceJoinOrder;
+            const lazy = !!paragraph.lazy;
+            const collocated = !!paragraph.collocated;
+
+            $scope.queryAvailable(paragraph) && 
_chooseNode(paragraph.cacheName, local)
+                .then((nid) => {
+                    // If we are executing only selected part of query then 
Notebook shouldn't be saved.
+                    if (!paragraph.partialQuery)
+                        
Notebook.save($scope.notebook).catch(Messages.showError);
+
+                    paragraph.localQueryMode = local;
+                    paragraph.prevQuery = paragraph.queryArgs ? 
paragraph.queryArgs.query : paragraph.query;
+
+                    _showLoading(paragraph, true);
+
+                    return _closeOldQuery(paragraph)
+                        .then(() => {
+                            const query = paragraph.partialQuery || 
paragraph.query;
+
+                            const args = paragraph.queryArgs = {
+                                type: 'QUERY',
+                                cacheName: $scope.cacheNameForSql(paragraph),
+                                query,
+                                pageSize: paragraph.pageSize,
+                                maxPages: paragraph.maxPages,
+                                nonCollocatedJoins,
+                                enforceJoinOrder,
+                                localNid: local ? nid : null,
+                                lazy,
+                                collocated
+                            };
+
+                            ActivitiesData.post({ group: 'sql', action: 
'/queries/execute' });
+
+                            const qry = args.maxPages ? addLimit(args.query, 
args.pageSize * args.maxPages) : query;
+
+                            return agentMgr.querySql(nid, args.cacheName, qry, 
nonCollocatedJoins, enforceJoinOrder, false, local, args.pageSize, lazy, 
collocated);
+                        })
+                        .then((res) => {
+                            _processQueryResult(paragraph, true, res);
+
+                            _tryStartRefresh(paragraph);
+                        })
+                        .catch((err) => {
+                            paragraph.setError(err);
+
+                            _showLoading(paragraph, false);
+
+                            $scope.stopRefresh(paragraph);
+                        })
+                        .then(() => paragraph.ace.focus());
+                });
+        };
+
+        const _cancelRefresh = (paragraph) => {
+            if (paragraph.rate && paragraph.rate.stopTime) {
+                delete paragraph.queryArgs;
+
+                paragraph.rate.installed = false;
+
+                $interval.cancel(paragraph.rate.stopTime);
+
+                delete paragraph.rate.stopTime;
+            }
+        };
+
+        $scope.explain = (paragraph) => {
+            const nonCollocatedJoins = !!paragraph.nonCollocatedJoins;
+            const enforceJoinOrder = !!paragraph.enforceJoinOrder;
+            const collocated = !!paragraph.collocated;
+
+            if (!$scope.queryAvailable(paragraph))
+                return;
+
+            if (!paragraph.partialQuery)
+                Notebook.save($scope.notebook).catch(Messages.showError);
+
+            _cancelRefresh(paragraph);
+
+            _showLoading(paragraph, true);
+
+            _closeOldQuery(paragraph)
+                .then(() => _chooseNode(paragraph.cacheName, false))
+                .then((nid) => {
+                    const args = paragraph.queryArgs = {
+                        type: 'EXPLAIN',
+                        cacheName: $scope.cacheNameForSql(paragraph),
+                        query: 'EXPLAIN ' + (paragraph.partialQuery || 
paragraph.query),
+                        pageSize: paragraph.pageSize
+                    };
+
+                    ActivitiesData.post({ group: 'sql', action: 
'/queries/explain' });
+
+                    return agentMgr.querySql(nid, args.cacheName, args.query, 
nonCollocatedJoins, enforceJoinOrder, false, false, args.pageSize, false, 
collocated);
+                })
+                .then((res) => _processQueryResult(paragraph, true, res))
+                .catch((err) => {
+                    paragraph.setError(err);
+
+                    _showLoading(paragraph, false);
+                })
+                .then(() => paragraph.ace.focus());
+        };
+
+        $scope.scan = (paragraph, local = false) => {
+            const cacheName = paragraph.cacheName;
+            const caseSensitive = !!paragraph.caseSensitive;
+            const filter = paragraph.filter;
+            const pageSize = paragraph.pageSize;
+
+            $scope.scanAvailable(paragraph) && _chooseNode(cacheName, local)
+                .then((nid) => {
+                    paragraph.localQueryMode = local;
+                    paragraph.scanningInProgress = true;
+
+                    Notebook.save($scope.notebook)
+                        .catch(Messages.showError);
+
+                    _cancelRefresh(paragraph);
+
+                    _showLoading(paragraph, true);
+
+                    _closeOldQuery(paragraph)
+                        .then(() => {
+                            paragraph.queryArgs = {
+                                type: 'SCAN',
+                                cacheName,
+                                filter,
+                                regEx: false,
+                                caseSensitive,
+                                near: false,
+                                pageSize,
+                                localNid: local ? nid : null
+                            };
+
+                            ActivitiesData.post({ group: 'sql', action: 
'/queries/scan' });
+
+                            return agentMgr.queryScan(nid, cacheName, filter, 
false, caseSensitive, false, local, pageSize);
+                        })
+                        .then((res) => _processQueryResult(paragraph, true, 
res))
+                        .catch((err) => {
+                            paragraph.setError(err);
+
+                            _showLoading(paragraph, false);
+                        })
+                        .then(() => paragraph.scanningInProgress = false);
+                });
+        };
+
+        function _updatePieChartsWithData(paragraph, newDatum) {
+            $timeout(() => {
+                _.forEach(paragraph.charts, function(chart) {
+                    const chartDatum = chart.data;
+
+                    chartDatum.length = 0;
+
+                    _.forEach(newDatum, function(series) {
+                        if (chart.options.title.text === series.key)
+                            _.forEach(series.values, (v) => 
chartDatum.push(v));
+                    });
+                });
+
+                _.forEach(paragraph.charts, (chart) => chart.api.update());
+            });
+        }
+
+        $scope.nextPage = (paragraph) => {
+            _showLoading(paragraph, true);
+
+            paragraph.queryArgs.pageSize = paragraph.pageSize;
+
+            agentMgr.queryNextPage(paragraph.resNodeId, paragraph.queryId, 
paragraph.pageSize)
+                .then((res) => {
+                    paragraph.page++;
+
+                    paragraph.total += paragraph.rows.length;
+
+                    paragraph.duration = res.duration;
+
+                    paragraph.rows = res.rows;
+
+                    if (paragraph.chart()) {
+                        if (paragraph.result === 'pie')
+                            _updatePieChartsWithData(paragraph, 
_pieChartDatum(paragraph));
+                        else
+                            _updateChartsWithData(paragraph, 
_chartDatum(paragraph));
+                    }
+
+                    paragraph.gridOptions.adjustHeight(paragraph.rows.length);
+
+                    _showLoading(paragraph, false);
+
+                    if (!res.hasMore)
+                        delete paragraph.queryId;
+                })
+                .catch((err) => {
+                    paragraph.setError(err);
+
+                    _showLoading(paragraph, false);
+                })
+                .then(() => paragraph.ace && paragraph.ace.focus());
+        };
+
+        const _export = (fileName, columnDefs, meta, rows, toClipBoard = 
false) => {
+            const csvSeparator = this.CSV.getSeparator();
+            let csvContent = '';
+
+            const cols = [];
+            const excludedCols = [];
+
+            _.forEach(meta, (col, idx) => {
+                if (columnDefs[idx].visible)
+                    cols.push(_fullColName(col));
+                else
+                    excludedCols.push(idx);
+            });
+
+            csvContent += cols.join(csvSeparator) + '\n';
+
+            _.forEach(rows, (row) => {
+                cols.length = 0;
+
+                if (Array.isArray(row)) {
+                    _.forEach(row, (elem, idx) => {
+                        if (_.includes(excludedCols, idx))
+                            return;
+
+                        cols.push(_.isUndefined(elem) ? '' : 
JSON.stringify(elem));
+                    });
+                }
+                else {
+                    _.forEach(columnDefs, (col) => {
+                        if (col.visible) {
+                            const elem = row[col.fieldName];
+
+                            cols.push(_.isUndefined(elem) ? '' : 
JSON.stringify(elem));
+                        }
+                    });
+                }
+
+                csvContent += cols.join(csvSeparator) + '\n';
+            });
+
+            if (toClipBoard)
+                IgniteCopyToClipboard.copy(csvContent);
+            else
+                LegacyUtils.download('text/csv', fileName, csvContent);
+        };
+
+        /**
+         * Generate file name with query results.
+         *
+         * @param paragraph {Object} Query paragraph .
+         * @param all {Boolean} All result export flag.
+         * @returns {string}
+         */
+        const exportFileName = (paragraph, all) => {
+            const args = paragraph.queryArgs;
+
+            if (args.type === 'SCAN')
+                return `export-scan-${args.cacheName}-${paragraph.name}${all ? 
'-all' : ''}.csv`;
+
+            return `export-query-${paragraph.name}${all ? '-all' : ''}.csv`;
+        };
+
+        $scope.exportCsvToClipBoard = (paragraph) => {
+            _export(exportFileName(paragraph, false), 
paragraph.gridOptions.columnDefs, paragraph.meta, paragraph.rows, true);
+        };
+
+        $scope.exportCsv = function(paragraph) {
+            _export(exportFileName(paragraph, false), 
paragraph.gridOptions.columnDefs, paragraph.meta, paragraph.rows);
+
+            // 
paragraph.gridOptions.api.exporter.csvExport(uiGridExporterConstants.ALL, 
uiGridExporterConstants.VISIBLE);
+        };
+
+        $scope.exportPdf = function(paragraph) {
+            
paragraph.gridOptions.api.exporter.pdfExport(uiGridExporterConstants.ALL, 
uiGridExporterConstants.VISIBLE);
+        };
+
+        $scope.exportCsvAll = (paragraph) => {
+            paragraph.csvIsPreparing = true;
+
+            const args = paragraph.queryArgs;
+
+            return Promise.resolve(args.localNid || 
_chooseNode(args.cacheName, false))
+                .then((nid) => args.type === 'SCAN'
+                    ? agentMgr.queryScanGetAll(nid, args.cacheName, 
args.query, !!args.regEx, !!args.caseSensitive, !!args.near, !!args.localNid)
+                    : agentMgr.querySqlGetAll(nid, args.cacheName, args.query, 
!!args.nonCollocatedJoins, !!args.enforceJoinOrder, false, !!args.localNid, 
!!args.lazy, !!args.collocated))
+                .then((res) => _export(exportFileName(paragraph, true), 
paragraph.gridOptions.columnDefs, res.columns, res.rows))
+                .catch(Messages.showError)
+                .then(() => {
+                    paragraph.csvIsPreparing = false;
+
+                    return paragraph.ace && paragraph.ace.focus();
+                });
+        };
+
+        // $scope.exportPdfAll = function(paragraph) {
+        //    $http.post('/api/v1/agent/query/getAll', {query: 
paragraph.query, cacheName: paragraph.cacheName})
+        //    .then(({data}) {
+        //        _export(paragraph.name + '-all.csv', data.meta, data.rows);
+        //    })
+        //    .catch(Messages.showError);
+        // };
+
+        $scope.rateAsString = function(paragraph) {
+            if (paragraph.rate && paragraph.rate.installed) {
+                const idx = _.findIndex($scope.timeUnit, function(unit) {
+                    return unit.value === paragraph.rate.unit;
+                });
+
+                if (idx >= 0)
+                    return ' ' + paragraph.rate.value + 
$scope.timeUnit[idx].short;
+
+                paragraph.rate.installed = false;
+            }
+
+            return '';
+        };
+
+        $scope.startRefresh = function(paragraph, value, unit) {
+            paragraph.rate.value = value;
+            paragraph.rate.unit = unit;
+            paragraph.rate.installed = true;
+
+            if (paragraph.queryExecuted() && !paragraph.scanExplain())
+                _tryStartRefresh(paragraph);
+        };
+
+        $scope.stopRefresh = function(paragraph) {
+            paragraph.rate.installed = false;
+
+            _tryStopRefresh(paragraph);
+        };
+
+        $scope.paragraphTimeSpanVisible = function(paragraph) {
+            return paragraph.timeLineSupported() && 
paragraph.chartTimeLineEnabled();
+        };
+
+        $scope.paragraphTimeLineSpan = function(paragraph) {
+            if (paragraph && paragraph.timeLineSpan)
+                return paragraph.timeLineSpan.toString();
+
+            return '1';
+        };
+
+        $scope.applyChartSettings = function(paragraph) {
+            _chartApplySettings(paragraph, true);
+        };
+
+        $scope.queryAvailable = function(paragraph) {
+            return paragraph.query && !paragraph.loading;
+        };
+
+        $scope.queryTooltip = function(paragraph, action) {
+            if ($scope.queryAvailable(paragraph))
+                return;
+
+            if (paragraph.loading)
+                return 'Waiting for server response';
+
+            return 'Input text to ' + action;
+        };
+
+        $scope.scanAvailable = function(paragraph) {
+            return $scope.caches.length && !(paragraph.loading || 
paragraph.csvIsPreparing);
+        };
+
+        $scope.scanTooltip = function(paragraph) {
+            if ($scope.scanAvailable(paragraph))
+                return;
+
+            if (paragraph.loading)
+                return 'Waiting for server response';
+
+            return 'Select cache to export scan results';
+        };
+
+        $scope.clickableMetadata = function(node) {
+            return node.type.slice(0, 5) !== 'index';
+        };
+
+        $scope.dblclickMetadata = function(paragraph, node) {
+            paragraph.ace.insert(node.name);
+
+            setTimeout(() => paragraph.ace.focus(), 1);
+        };
+
+        $scope.importMetadata = function() {
+            Loading.start('loadingCacheMetadata');
+
+            $scope.metadata = [];
+
+            agentMgr.metadata()
+                .then((metadata) => {
+                    $scope.metadata = _.sortBy(_.filter(metadata, (meta) => {
+                        const cache = _.find($scope.caches, { value: 
meta.cacheName });
+
+                        if (cache) {
+                            meta.name = (cache.sqlSchema || '"' + 
meta.cacheName + '"') + '.' + meta.typeName;
+                            meta.displayName = (cache.sqlSchema || 
meta.maskedName) + '.' + meta.typeName;
+
+                            if (cache.sqlSchema)
+                                meta.children.unshift({type: 'plain', name: 
'cacheName: ' + meta.maskedName, maskedName: meta.maskedName});
+
+                            meta.children.unshift({type: 'plain', name: 'mode: 
' + cache.mode, maskedName: meta.maskedName});
+                        }
+
+                        return cache;
+                    }), 'name');
+                })
+                .catch(Messages.showError)
+                .then(() => Loading.finish('loadingCacheMetadata'));
+        };
+
+        $scope.showResultQuery = function(paragraph) {
+            if (!_.isNil(paragraph)) {
+                const scope = $scope.$new();
+
+                if (paragraph.queryArgs.type === 'SCAN') {
+                    scope.title = 'SCAN query';
+
+                    const filter = paragraph.queryArgs.filter;
+
+                    if (_.isEmpty(filter))
+                        scope.content = [`SCAN query for cache: 
<b>${maskCacheName(paragraph.queryArgs.cacheName, true)}</b>`];
+                    else
+                        scope.content = [`SCAN query for cache: 
<b>${maskCacheName(paragraph.queryArgs.cacheName, true)}</b> with filter: 
<b>${filter}</b>`];
+                }
+                else if (paragraph.queryArgs.query .startsWith('EXPLAIN ')) {
+                    scope.title = 'Explain query';
+                    scope.content = paragraph.queryArgs.query.split(/\r?\n/);
+                }
+                else {
+                    scope.title = 'SQL query';
+                    scope.content = paragraph.queryArgs.query.split(/\r?\n/);
+                }
+
+                // Attach duration and selected node info
+                scope.meta = `Duration: 
${$filter('duration')(paragraph.duration)}.`;
+                scope.meta += paragraph.localQueryMode ? ` Node ID8: 
${id8(paragraph.resNodeId)}` : '';
+
+                // Show a basic modal from a controller
+                $modal({scope, templateUrl: messageTemplateUrl, show: true});
+            }
+        };
+
+        $scope.showStackTrace = function(paragraph) {
+            if (!_.isNil(paragraph)) {
+                const scope = $scope.$new();
+
+                scope.title = 'Error details';
+                scope.content = [];
+
+                const tab = '&nbsp;&nbsp;&nbsp;&nbsp;';
+
+                const addToTrace = (item) => {
+                    if (nonNil(item)) {
+                        scope.content.push((scope.content.length > 0 ? tab : 
'') + errorParser.extractFullMessage(item));
+
+                        addToTrace(item.cause);
+
+                        _.forEach(item.suppressed, (sup) => addToTrace(sup));
+                    }
+                };
+
+                addToTrace(paragraph.error.root);
+
+                // Show a basic modal from a controller
+                $modal({scope, templateUrl: messageTemplateUrl, show: true});
+            }
+        };
+    }
+
+    scanActions: QueryActions<Paragraph & {type: 'scan'}> = [
+        {
+            text: 'Scan',
+            click: (p) => this.$scope.scan(p),
+            available: (p) => this.$scope.scanAvailable(p)
+        },
+        {
+            text: 'Scan on selected node',
+            click: (p) => this.$scope.scan(p, true),
+            available: (p) => this.$scope.scanAvailable(p)
+        },
+        {text: 'Rename', click: (p) => this.renameParagraph(p), available: () 
=> true},
+        {text: 'Remove', click: (p) => this.removeParagraph(p), available: () 
=> true}
+    ];
+
+    queryActions: QueryActions<Paragraph & {type: 'query'}> = [
+        {
+            text: 'Execute',
+            click: (p) => this.$scope.execute(p),
+            available: (p) => this.$scope.queryAvailable(p)
+        },
+        {
+            text: 'Execute on selected node',
+            click: (p) => this.$scope.execute(p, true),
+            available: (p) => this.$scope.queryAvailable(p)
+        },
+        {
+            text: 'Explain',
+            click: (p) => this.$scope.explain(p),
+            available: (p) => this.$scope.queryAvailable(p)
+        },
+        {text: 'Rename', click: (p) => this.renameParagraph(p), available: () 
=> true},
+        {text: 'Remove', click: (p) => this.removeParagraph(p), available: () 
=> true}
+    ];
+
+    async renameParagraph(paragraph: Paragraph) {
+        try {
+            const newName = await this.IgniteInput.input('Rename Query', 'New 
query name:', paragraph.name);
+
+            if (paragraph.name !== newName) {
+                paragraph.name = newName;
+
+                this.$scope.rebuildScrollParagraphs();
+
+                await this.Notebook.save(this.$scope.notebook)
+                    .catch(this.Messages.showError);
+            }
+        }
+        catch (ignored) {
+            // No-op.
+        }
+    }
+
+    async removeParagraph(paragraph: Paragraph) {
+        try {
+            await this.Confirm.confirm('Are you sure you want to remove query: 
"' + paragraph.name + '"?');
+            this.$scope.stopRefresh(paragraph);
+
+            const paragraph_idx = _.findIndex(this.$scope.notebook.paragraphs, 
(item) => paragraph === item);
+            const panel_idx = _.findIndex(this.$scope.expandedParagraphs, 
(item) => paragraph_idx === item);
+
+            if (panel_idx >= 0)
+                this.$scope.expandedParagraphs.splice(panel_idx, 1);
+
+            this.$scope.notebook.paragraphs.splice(paragraph_idx, 1);
+            this.$scope.rebuildScrollParagraphs();
+
+            await this.Notebook.save(this.$scope.notebook)
+                .catch(this.Messages.showError);
+        }
+        catch (ignored) {
+            // No-op.
+        }
+    }
+
+    isParagraphOpened(index: number) {
+        return this.$scope.notebook.expandedParagraphs.includes(index);
+    }
+
+    onParagraphClose(index: number) {
+        const expanded = this.$scope.notebook.expandedParagraphs;
+        expanded.splice(expanded.indexOf(index), 1);
+    }
+
+    onParagraphOpen(index: number) {
+        this.$scope.notebook.expandedParagraphs.push(index);
+    }
+
+    $onDestroy() {
+        if (this.refresh$)
+            this.refresh$.unsubscribe();
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/e1b8686a/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/index.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/index.js
 
b/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/index.js
index bf93f42..57aa779 100644
--- 
a/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/index.js
+++ 
b/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/index.js
@@ -19,9 +19,11 @@ import angular from 'angular';
 import templateUrl from './template.tpl.pug';
 import { NotebookCtrl } from './controller';
 import NotebookData from '../../notebook.data';
+import {component as actions} from 
'./components/query-actions-button/component';
 import './style.scss';
 
 export default angular.module('ignite-console.sql.notebook', [])
+    .component('queryActionsButton', actions)
     .component('queriesNotebook', {
         controller: NotebookCtrl,
         templateUrl

http://git-wip-us.apache.org/repos/asf/ignite/blob/e1b8686a/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/template.tpl.pug
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/template.tpl.pug
 
b/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/template.tpl.pug
index af673a8..e2c4408 100644
--- 
a/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/template.tpl.pug
+++ 
b/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/template.tpl.pug
@@ -71,24 +71,6 @@ mixin notebook-error
     label.col-sm-12 Notebook not accessible any more. Go back to notebooks 
list.
     button.h3.btn.btn-primary(ui-sref='default-state') Leave Notebook
 
-mixin paragraph-rename
-    .col-sm-6(ng-hide='paragraph.edit')
-        i.fa(ng-class='paragraphExpanded(paragraph) ? "fa-chevron-circle-down" 
: "fa-chevron-circle-right"')
-        label {{paragraph.name}}
-
-        .btn-group(ng-hide='notebook.paragraphs.length > 1')
-            +btn-toolbar('fa-pencil', 'paragraph.edit = true; 
paragraph.editName = paragraph.name; $event.stopPropagation();', 'Rename 
query', 'paragraph-name-{{paragraph.id}}')
-
-        .btn-group(ng-show='notebook.paragraphs.length > 1' 
ng-click='$event.stopPropagation();')
-            +btn-toolbar('fa-pencil', 'paragraph.edit = true; 
paragraph.editName = paragraph.name;', 'Rename query', 
'paragraph-name-{{paragraph.id}}')
-            +btn-toolbar('fa-remove', 'removeParagraph(paragraph)', 'Remove 
query')
-
-    .col-sm-6(ng-show='paragraph.edit')
-        i.tipLabel.fa(style='float: left;' 
ng-class='paragraphExpanded(paragraph) ? "fa-chevron-circle-down" : 
"fa-chevron-circle-right"')
-        i.tipLabel.fa.fa-floppy-o(style='float: right;' 
ng-show='paragraph.editName' ng-click='renameParagraph(paragraph, 
paragraph.editName); $event.stopPropagation();' bs-tooltip data-title='Save 
query name' data-trigger='hover')
-        .input-tip
-            input.form-control(id='paragraph-name-{{paragraph.id}}' 
ng-model='paragraph.editName' required ng-click='$event.stopPropagation();' 
ignite-on-enter='renameParagraph(paragraph, paragraph.editName)' 
ignite-on-escape='paragraph.edit = false')
-
 mixin query-settings
     div
         .form-field--inline(
@@ -314,10 +296,10 @@ mixin chart-result
         label.margin-top-dflt Charts do not support #[b Explain] and #[b Scan] 
query
 
 mixin paragraph-scan
-    .panel-heading(bs-collapse-toggle)
-        .row
-            +paragraph-rename
-    .panel-collapse(role='tabpanel' bs-collapse-target)
+    panel-title {{ paragraph.name }}
+    panel-actions
+        query-actions-button(actions='$ctrl.scanActions' item='paragraph')
+    panel-content
         .col-sm-12.sql-controls
             .col-sm-3
                 +form-field__dropdown({
@@ -378,9 +360,10 @@ mixin paragraph-scan
                     a Next
 
 mixin paragraph-query
-    .row.panel-heading(bs-collapse-toggle)
-        +paragraph-rename
-    .panel-collapse.ng-animate-disabled(role='tabpanel' bs-collapse-target)
+    panel-title {{ paragraph.name }}
+    panel-actions
+        query-actions-button(actions='$ctrl.queryActions' item='paragraph')
+    panel-content
         .col-sm-12
             .col-xs-8.col-sm-9(style='border-right: 1px solid #eee')
                 .sql-editor(ignite-ace='{onLoad: aceInit(paragraph), theme: 
"chrome", mode: "sql", require: ["ace/ext/language_tools"],' +
@@ -510,10 +493,19 @@ div
 
     div(ng-if='notebook' ignite-loading='sqlLoading' ignite-loading-text='{{ 
loadingText }}' ignite-loading-position='top')
         .docs-body.paragraphs
-            .panel-group(bs-collapse ng-model='notebook.expandedParagraphs' 
data-allow-multiple='true' data-start-collapsed='false')
-
+            .panel-group
                 .panel-paragraph(ng-repeat='paragraph in notebook.paragraphs' 
id='{{paragraph.id}}' ng-form='form_{{paragraph.id}}')
-                    .panel.panel-default(ng-if='paragraph.qryType === "scan"')
+                    panel-collapsible(
+                        ng-if='paragraph.qryType === "scan"'
+                        opened='$ctrl.isParagraphOpened($index)'
+                        on-close='$ctrl.onParagraphClose($index)'
+                        on-open='$ctrl.onParagraphOpen($index)'
+                    )
                         +paragraph-scan
-                    .panel.panel-default(ng-if='paragraph.qryType === "query"')
+                    panel-collapsible(
+                        ng-if='paragraph.qryType === "query"'
+                        opened='$ctrl.isParagraphOpened($index)'
+                        on-close='$ctrl.onParagraphClose($index)'
+                        on-open='$ctrl.onParagraphOpen($index)'
+                    )
                         +paragraph-query

Reply via email to