constructor()

in modules/frontend/app/components/page-queries/components/queries-notebook/controller.ts [310:2218]


    constructor(private IgniteInput: InputDialog, $root, private $scope, $http, $q, $timeout, $transitions, $interval, $animate, $location, $anchorScroll, $state, $filter, $modal, $popover, $window, 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, $transitions, $interval, $animate, $location, $anchorScroll, $state, $filter, $modal, $popover, $window, 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);
        };

        this._stopTopologyRefresh = () => {
            if ($scope.notebook && $scope.notebook.paragraphs)
                $scope.notebook.paragraphs.forEach((paragraph) => _tryStopRefresh(paragraph));
        };

        $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 finishLoading$ = defer(() => {
                if (!$root.IgniteDemoMode)
                    Loading.finish('sqlLoading');
            }).pipe(take(1));

            const refreshCaches = (period) => {
                return merge(timer(0, period).pipe(exhaustMap(() => _refreshCaches())), finishLoading$);
            };

            const cluster$ = agentMgr.connectionSbj.pipe(
                pluck('cluster'),
                distinctUntilChanged(),
                tap((cluster) => {
                    this.clusterIsAvailable = (!!cluster && cluster.active === true) || agentMgr.isDemoMode();
                })
            );

            this.refresh$ = cluster$.pipe(
                switchMap((cluster) => {
                    if (!cluster && !agentMgr.isDemoMode()) {
                        return of(EMPTY).pipe(
                            tap(() => {
                                $scope.caches = [];
                            })
                        );
                    }

                    return of(cluster).pipe(
                        tap(() => Loading.start('sqlLoading')),
                        tap(() => {
                            _.forEach($scope.notebook.paragraphs, (paragraph) => {
                                paragraph.reset($interval);
                            });
                        }),
                        switchMap(() => refreshCaches(5000))
                    );
                })
            );

            this.subscribers$ = merge(this.refresh$).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);
        };

        /**
         * Execute query and get first result page.
         *
         * @param qryType Query type. 'query' or `scan`.
         * @param qryArg Argument with query properties.
         * @param {(res) => any} onQueryStarted Action to execute when query ID is received.
         * @return {Observable<VisorQueryResult>} Observable with first query result page.
         */
        const _executeQuery0 = (qryType, qryArg, onQueryStarted: (res) => any = () => {}) => {
            return from(qryType === 'scan' ? agentMgr.queryScan(qryArg) : agentMgr.querySql(qryArg)).pipe(
                tap((res) => {
                    onQueryStarted(res);
                    $scope.$applyAsync();
                }),
                exhaustMap((res) => {
                    if (!_.isNil(res.rows))
                        return of(res);

                    const fetchFirstPageTask = timer(100, 500).pipe(
                        exhaustMap(() => agentMgr.queryFetchFistsPage(qryArg.nid, res.queryId, qryArg.pageSize)),
                        filter((res) => !_.isNil(res.rows))
                    );

                    const pingQueryTask = timer(60000, 60000).pipe(
                        exhaustMap(() => agentMgr.queryPing(qryArg.nid, res.queryId)),
                        takeWhile(({queryPingSupported}) => queryPingSupported),
                        ignoreElements()
                    );

                    return merge(fetchFirstPageTask, pingQueryTask);
                }),
                first()
            );
        };

        /**
         * Execute query with old query clearing and showing of query result.
         *
         * @param paragraph Query paragraph.
         * @param qryArg Argument with query properties.
         * @param {(res) => any} onQueryStarted Action to execute when query ID is received.
         * @param {(res) => any} onQueryFinished Action to execute when first query result page is received.
         * @param {(err) => any} onError Action to execute when error occured.
         * @return {Observable<VisorQueryResult>} Observable with first query result page.
         */
        const _executeQuery = (
            paragraph,
            qryArg,
            onQueryStarted: (res) => any = () => {},
            onQueryFinished: (res) => any = () => {},
            onError: (err) => any = () => {}
        ) => {
            return from(_closeOldQuery(paragraph)).pipe(
                switchMap(() => _executeQuery0(paragraph.qryType, qryArg, onQueryStarted)),
                tap((res) => {
                    onQueryFinished(res);
                    $scope.$applyAsync();
                }),
                takeUntil(paragraph.cancelQuerySubject),
                catchError((err) => {
                    onError(err);
                    $scope.$applyAsync();

                    return of(err);
                })
            );
        };

        /**
         * Execute query and get all query results.
         *
         * @param paragraph Query paragraph.
         * @param qryArg Argument with query properties.
         * @param {(res) => any} onQueryStarted Action to execute when query ID is received.
         * @param {(res) => any} onQueryFinished Action to execute when first query result page is received.
         * @param {(err) => any} onError Action to execute when error occured.
         * @return {Observable<any>} Observable with full query result.
         */
        const _exportQueryAll = (
            paragraph,
            qryArg,
            onQueryStarted: (res) => any = () => {},
            onQueryFinished: (res) => any = () => {},
            onError: (err) => any = () => {}
        ) => {
            return from(_closeOldExport(paragraph)).pipe(
                switchMap(() => _executeQuery0(paragraph.qryType, qryArg, onQueryStarted)),
                expand((acc) => {
                    return from(agentMgr.queryNextPage(acc.responseNodeId, acc.queryId, qryArg.pageSize)
                        .then((res) => {
                            acc.rows = acc.rows.concat(res.rows);
                            acc.hasMore = res.hasMore;

                            return acc;
                        }));
                }),
                first((acc) => !acc.hasMore),
                tap(onQueryFinished),
                takeUntil(paragraph.cancelExportSubject),
                catchError((err) => {
                    onError(err);

                    return of(err);
                })
            );
        };

        /**
         * @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();

            paragraph.showLoading(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 _fetchQueryResult = (paragraph, clearChart, res) => {
            _processQueryResult(paragraph, clearChart, res);
            _tryStartRefresh(paragraph);
        };

        const _closeOldQuery = (paragraph) => {
            const nid = paragraph.resNodeId;

            if (paragraph.queryId) {
                const qryId = paragraph.queryId;
                delete paragraph.queryId;

                return agentMgr.queryClose(nid, qryId);
            }

            return $q.when();
        };

        const _closeOldExport = (paragraph) => {
            const nid = paragraph.exportNodeId;

            if (paragraph.exportId) {
                const exportId = paragraph.exportId;
                delete paragraph.exportId;

                return agentMgr.queryClose(nid, exportId);
            }

            return $q.when();
        };

        $scope.cancelQuery = (paragraph) => {
            paragraph.cancelQuerySubject.next(true);

            this.$scope.stopRefresh(paragraph);

            _closeOldQuery(paragraph)
                .catch((err) => paragraph.setError(err))
                .finally(() => paragraph.showLoading(false));
        };

        /**
         * @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)];
                })
                .catch(Messages.showError);
        };

        const _executeRefresh = (paragraph) => {
            const args = paragraph.queryArgs;

            from(agentMgr.awaitCluster()).pipe(
                switchMap(() => args.localNid ? of(args.localNid) : from(_chooseNode(args.cacheName, false))),
                switchMap((nid) => {
                    paragraph.showLoading(true);

                    const qryArg = {
                        nid,
                        cacheName: args.cacheName,
                        query: args.query,
                        nonCollocatedJoins: args.nonCollocatedJoins,
                        enforceJoinOrder: args.enforceJoinOrder,
                        replicatedOnly: false,
                        local: !!args.localNid,
                        pageSize: args.pageSize,
                        lazy: args.lazy,
                        collocated: args.collocated
                    };

                    return _executeQuery(
                        paragraph,
                        qryArg,
                        (res) => _initQueryResult(paragraph, res),
                        (res) => _fetchQueryResult(paragraph, false, res),
                        (err) => {
                            paragraph.setError(err);
                            paragraph.ace && paragraph.ace.focus();
                            $scope.stopRefresh(paragraph);
                        }
                    );
                }),
                finalize(() => paragraph.showLoading(false))
            ).toPromise();
        };

        const _tryStartRefresh = function(paragraph) {
            if (_.get(paragraph, 'rate.installed') && paragraph.queryExecuted() && paragraph.nonRefresh()) {
                $scope.chartAcceptKeyColumn(paragraph, TIME_LINE);

                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;
        };

        const _initQueryResult = (paragraph, res) => {
            paragraph.resNodeId = res.responseNodeId;
            paragraph.queryId = res.queryId;

            if (paragraph.nonRefresh()) {
                paragraph.rows = [];

                paragraph.meta = [];
                paragraph.setError({message: ''});
            }

            paragraph.hasNext = false;
        };

        const _initExportResult = (paragraph, res) => {
            paragraph.exportNodeId = res.responseNodeId;
            paragraph.exportId = res.queryId;
        };

        $scope.execute = (paragraph, local = false) => {
            if (!$scope.queryAvailable(paragraph))
                return;

            const nonCollocatedJoins = !!paragraph.nonCollocatedJoins;
            const enforceJoinOrder = !!paragraph.enforceJoinOrder;
            const lazy = !!paragraph.lazy;
            const collocated = !!paragraph.collocated;

            _cancelRefresh(paragraph);

            from(_chooseNode(paragraph.cacheName, local)).pipe(
                switchMap((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;

                    paragraph.showLoading(true);

                    const query = paragraph.partialQuery || paragraph.query;

                    const args = paragraph.queryArgs = {
                        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;
                    const qryArg = {
                        nid,
                        cacheName: args.cacheName,
                        query: qry,
                        nonCollocatedJoins,
                        enforceJoinOrder,
                        replicatedOnly: false,
                        local,
                        pageSize: args.pageSize,
                        lazy,
                        collocated
                    };

                    return _executeQuery(
                        paragraph,
                        qryArg,
                        (res) => _initQueryResult(paragraph, res),
                        (res) => _fetchQueryResult(paragraph, true, res),
                        (err) => {
                            paragraph.setError(err);
                            paragraph.ace && paragraph.ace.focus();
                            $scope.stopRefresh(paragraph);

                            Messages.showError(err);
                        }
                    );
                }),
                finalize(() => paragraph.showLoading(false))
            ).toPromise();
        };

        const _cancelRefresh = (paragraph) => {
            if (paragraph.rate && paragraph.rate.stopTime) {
                delete paragraph.queryArgs;

                _.set(paragraph, 'rate.installed', false);

                $interval.cancel(paragraph.rate.stopTime);

                delete paragraph.rate.stopTime;
            }
        };

        $scope.explain = (paragraph) => {
            if (!$scope.queryAvailable(paragraph))
                return;

            const nonCollocatedJoins = !!paragraph.nonCollocatedJoins;
            const enforceJoinOrder = !!paragraph.enforceJoinOrder;
            const collocated = !!paragraph.collocated;

            if (!paragraph.partialQuery)
                Notebook.save($scope.notebook).catch(Messages.showError);

            _cancelRefresh(paragraph);

            paragraph.showLoading(true);

            from(_chooseNode(paragraph.cacheName, false)).pipe(
                switchMap((nid) => {
                    const qryArg = paragraph.queryArgs = {
                        nid,
                        cacheName: $scope.cacheNameForSql(paragraph),
                        query: 'EXPLAIN ' + (paragraph.partialQuery || paragraph.query),
                        nonCollocatedJoins,
                        enforceJoinOrder,
                        replicatedOnly: false,
                        local: false,
                        pageSize: paragraph.pageSize,
                        lazy: false,
                        collocated
                    };

                    ActivitiesData.post({ group: 'sql', action: '/queries/explain' });

                    return _executeQuery(
                        paragraph,
                        qryArg,
                        (res) => _initQueryResult(paragraph, res),
                        (res) => _fetchQueryResult(paragraph, true, res),
                        (err) => {
                            paragraph.setError(err);
                            paragraph.ace && paragraph.ace.focus();
                        }
                    );
                }),
                finalize(() => paragraph.showLoading(false))
            ).toPromise();
        };

        $scope.scan = (paragraph, local = false) => {
            if (!$scope.scanAvailable(paragraph))
                return;

            const cacheName = paragraph.cacheName;
            const caseSensitive = !!paragraph.caseSensitive;
            const filter = paragraph.filter;
            const pageSize = paragraph.pageSize;

            from(_chooseNode(cacheName, local)).pipe(
                switchMap((nid) => {
                    paragraph.localQueryMode = local;

                    Notebook.save($scope.notebook)
                        .catch(Messages.showError);

                    paragraph.showLoading(true);

                    const qryArg = paragraph.queryArgs = {
                        cacheName,
                        filter,
                        regEx: false,
                        caseSensitive,
                        near: false,
                        pageSize,
                        localNid: local ? nid : null
                    };

                    qryArg.nid = nid;
                    qryArg.local = local;

                    ActivitiesData.post({ group: 'sql', action: '/queries/scan' });

                    return _executeQuery(
                        paragraph,
                        qryArg,
                        (res) => _initQueryResult(paragraph, res),
                        (res) => _fetchQueryResult(paragraph, true, res),
                        (err) => paragraph.setError(err)
                    );
                }),
                finalize(() => paragraph.showLoading(false))
            ).toPromise();
        };

        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());
            });
        }

        const _processQueryNextPage = (paragraph, 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);

            paragraph.showLoading(false);

            if (!res.hasMore)
                delete paragraph.queryId;
        };

        $scope.nextPage = (paragraph) => {
            paragraph.showLoading(true);

            paragraph.queryArgs.pageSize = paragraph.pageSize;

            const nextPageTask = from(agentMgr.queryNextPage(paragraph.resNodeId, paragraph.queryId, paragraph.pageSize)
                .then((res) => _processQueryNextPage(paragraph, res))
                .catch((err) => {
                    paragraph.setError(err);
                    paragraph.ace && paragraph.ace.focus();
                }));

            const pingQueryTask = timer(60000, 60000).pipe(
                exhaustMap(() => agentMgr.queryPing(paragraph.resNodeId, paragraph.queryId)),
                takeWhile(({queryPingSupported}) => queryPingSupported),
                ignoreElements()
            );

            merge(nextPageTask, pingQueryTask).pipe(
                take(1),
                takeUntil(paragraph.cancelQuerySubject)
            ).subscribe();
        };

        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 (paragraph.qryType === '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) => {
            const args = paragraph.queryArgs;

            paragraph.cancelExportSubject.next(true);

            paragraph.csvIsPreparing = true;

            return (args.localNid ? of(args.localNid) : from(_chooseNode(args.cacheName, false))).pipe(
                map((nid) => _.assign({}, args, {nid, pageSize: 1024, local: !!args.localNid, replicatedOnly: false})),
                switchMap((arg) => _exportQueryAll(
                    paragraph,
                    arg,
                    (res) => _initExportResult(paragraph, res),
                    (res) => _export(exportFileName(paragraph, true), paragraph.gridOptions.columnDefs, res.columns, res.rows),
                    (err) => {
                        Messages.showError(err);
                        return of(err);
                    }
                )),
                finalize(() => paragraph.csvIsPreparing = false)
            ).toPromise();
        };

        // $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) {
            $scope.stopRefresh(paragraph);

            paragraph.rate.value = value;
            paragraph.rate.unit = unit;
            paragraph.rate.installed = true;

            if (paragraph.queryExecuted() && !paragraph.scanExplain())
                _executeRefresh(paragraph);
        };

        $scope.stopRefresh = function(paragraph) {
            _.set(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(), 100);
        };

        $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.qryType === '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});
            }
        };

        this.offTransitions = $transitions.onBefore({from: 'base.sql.notebook'}, ($transition$) => {
            const options = $transition$.options();

            // Skip query closing in case of auto redirection on state change.
            if (options.redirectedFrom)
                return true;

            return this.closeOpenedQueries();
        });

        $window.addEventListener('beforeunload', this.closeOpenedQueries);

        this.onClusterSwitchLnr = () => {
            const paragraphs = _.get(this, '$scope.notebook.paragraphs');

            if (this._hasRunningQueries(paragraphs)) {
                try {
                    return Confirm.confirm('You have running queries. Are you sure you want to cancel them?')
                        .then(() => this._closeOpenedQueries(paragraphs));
                }
                catch (err) {
                    return Promise.reject(new CancellationError());
                }
            }

            return Promise.resolve(true);
        };

        agentMgr.addClusterSwitchListener(this.onClusterSwitchLnr);
    }