You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ak...@apache.org on 2017/12/06 03:37:39 UTC

[1/5] ignite git commit: IGNITE-6390 Web Console: Added component for cluster selection.

Repository: ignite
Updated Branches:
  refs/heads/master cbd69d6b3 -> 1367bc98e


http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/views/sql/sql.tpl.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/views/sql/sql.tpl.pug b/modules/web-console/frontend/views/sql/sql.tpl.pug
deleted file mode 100644
index 98b4d68..0000000
--- a/modules/web-console/frontend/views/sql/sql.tpl.pug
+++ /dev/null
@@ -1,381 +0,0 @@
-//-
-    Licensed to the Apache Software Foundation (ASF) under one or more
-    contributor license agreements.  See the NOTICE file distributed with
-    this work for additional information regarding copyright ownership.
-    The ASF licenses this file to You under the Apache License, Version 2.0
-    (the "License"); you may not use this file except in compliance with
-    the License.  You may obtain a copy of the License at
-
-         http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
-
-include /app/helpers/jade/mixins
-
-mixin btn-toolbar(btn, click, tip, focusId)
-    i.btn.btn-default.fa(class=btn ng-click=click bs-tooltip='' data-title=tip ignite-on-click-focus=focusId data-trigger='hover' data-placement='bottom')
-
-mixin btn-toolbar-data(btn, kind, tip)
-    i.btn.btn-default.fa(class=btn ng-click=`setResult(paragraph, '${kind}')` ng-class=`{active: resultEq(paragraph, '${kind}')}` bs-tooltip='' data-title=tip data-trigger='hover' data-placement='bottom')
-
-mixin result-toolbar
-    .btn-group(ng-model='paragraph.result' ng-click='$event.stopPropagation()' style='left: 50%; margin: 0 0 0 -70px;display: block;')
-        +btn-toolbar-data('fa-table', 'table', 'Show data in tabular form')
-        +btn-toolbar-data('fa-bar-chart', 'bar', 'Show bar chart<br/>By default first column - X values, second column - Y values<br/>In case of one column it will be treated as Y values')
-        +btn-toolbar-data('fa-pie-chart', 'pie', 'Show pie chart<br/>By default first column - pie labels, second column - pie values<br/>In case of one column it will be treated as pie values')
-        +btn-toolbar-data('fa-line-chart', 'line', 'Show line chart<br/>By default first column - X values, second column - Y values<br/>In case of one column it will be treated as Y values')
-        +btn-toolbar-data('fa-area-chart', 'area', 'Show area chart<br/>By default first column - X values, second column - Y values<br/>In case of one column it will be treated as Y values')
-
-mixin chart-settings
-    .total.row
-        .col-xs-7
-            .chart-settings-link(ng-show='paragraph.chart && paragraph.chartColumns.length > 0')
-                a(title='Click to show chart settings dialog' ng-click='$event.stopPropagation()' bs-popover data-template-url='{{ $ctrl.chartSettingsTemplateUrl }}' data-placement='bottom' data-auto-close='1' data-trigger='click')
-                    i.fa.fa-bars
-                    | Chart settings
-                div(ng-show='paragraphTimeSpanVisible(paragraph)')
-                    label Show
-                    button.select-manual-caret.btn.btn-default(ng-model='paragraph.timeLineSpan' ng-change='applyChartSettings(paragraph)' bs-options='item for item in timeLineSpans' bs-select data-caret-html='<span class="caret"></span>')
-                    label min
-
-                div
-                    label Duration: #[b {{paragraph.duration | duration}}]
-                    label.margin-left-dflt(ng-show='paragraph.localQueryMode') NodeID8: #[b {{paragraph.resNodeId | id8}}]
-        .col-xs-2
-            +result-toolbar
-
-mixin notebook-rename
-    .docs-header.notebook-header
-        h1.col-sm-6(ng-hide='notebook.edit')
-            label(style='max-width: calc(100% - 60px)') {{notebook.name}}
-            .btn-group(ng-if='!demo')
-                +btn-toolbar('fa-pencil', 'notebook.edit = true;notebook.editName = notebook.name', 'Rename notebook')
-                +btn-toolbar('fa-trash', 'removeNotebook(notebook)', 'Remove notebook')
-        h1.col-sm-6(ng-show='notebook.edit')
-            i.btn.fa.fa-floppy-o(ng-show='notebook.editName' ng-click='renameNotebook(notebook.editName)' bs-tooltip data-title='Save notebook name' data-trigger='hover')
-            .input-tip
-                input.form-control(ng-model='notebook.editName' required ignite-on-enter='renameNotebook(notebook.editName)' ignite-on-escape='notebook.edit = false;')
-        h1.pull-right
-            a.dropdown-toggle(style='margin-right: 20px' data-toggle='dropdown' bs-dropdown='scrollParagraphs' data-placement='bottom-right') Scroll to query
-                span.caret
-            button.btn.btn-default(style='margin-top: 2px' ng-click='addQuery()' ignite-on-click-focus=focusId)
-                i.fa.fa-fw.fa-plus
-                | Add query
-
-            button.btn.btn-default(style='margin-top: 2px' ng-click='addScan()' ignite-on-click-focus=focusId)
-                i.fa.fa-fw.fa-plus
-                | Add scan
-
-mixin notebook-error
-    h2 Failed to load notebook
-    label.col-sm-12 Notebook not accessible any more. Go back to configuration or open to another notebook.
-    button.h3.btn.btn-primary(ui-sref='base.configuration.tabs.advanced.clusters') Back to configuration
-
-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
-    .panel-top-align
-        label.tipLabel(bs-tooltip data-placement='bottom' data-title='Configure periodical execution of last successfully executed query') Refresh rate:
-            button.btn.btn-default.fa.fa-clock-o.tipLabel(ng-class='{"btn-info": paragraph.rate && paragraph.rate.installed}' bs-popover data-template-url='{{ $ctrl.paragraphRateTemplateUrl }}' data-placement='left' data-auto-close='1' data-trigger='click') {{rateAsString(paragraph)}}
-
-        label.tipLabel(bs-tooltip data-placement='bottom' data-title='Max number of rows to show in query result as one page') Page size:
-            button.btn.btn-default.select-toggle.tipLabel(ng-model='paragraph.pageSize' bs-select bs-options='item for item in pageSizes')
-
-        label.tipLabel(bs-tooltip data-placement='bottom' data-title='Limit query max results to specified number of pages') Max pages:
-            button.btn.btn-default.select-toggle.tipLabel(ng-model='paragraph.maxPages' bs-select bs-options='item.value as item.label for item in maxPages')
-
-        .panel-tip-container
-            .row(ng-if='nonCollocatedJoinsAvailable(paragraph)')
-                label.tipLabel(bs-tooltip data-placement='bottom' data-title='Non-collocated joins is a special mode that allow to join data across cluster without collocation.<br/>\
-                    Nested joins are not supported for now.<br/>\
-                    <b>NOTE</b>: In some cases it may consume more heap memory or may take a long time than collocated joins.' data-trigger='hover')
-                    input(type='checkbox' ng-model='paragraph.nonCollocatedJoins')
-                    span Allow non-collocated joins
-            .row(ng-if='enforceJoinOrderAvailable(paragraph)')
-                label.tipLabel(bs-tooltip data-placement='bottom' data-title='Enforce join order of tables in the query.<br/>\
-                    If <b>set</b>, then query optimizer will not reorder tables within join.<br/>\
-                    <b>NOTE:</b> It is not recommended to enable this property unless you have verified that\
-                    indexes are not selected in optimal order.' data-trigger='hover')
-                    input(type='checkbox' ng-model='paragraph.enforceJoinOrder')
-                    span Enforce join order
-            .row(ng-if='lazyQueryAvailable(paragraph)')
-                label.tipLabel(bs-tooltip data-placement='bottom' data-title='By default Ignite attempts to fetch the whole query result set to memory and send it to the client.<br/>\
-                    For small and medium result sets this provides optimal performance and minimize duration of internal database locks, thus increasing concurrency.<br/>\
-                    If result set is too big to fit in available memory this could lead to excessive GC pauses and even OutOfMemoryError.<br/>\
-                    Use this flag as a hint for Ignite to fetch result set lazily, thus minimizing memory consumption at the cost of moderate performance hit.' data-trigger='hover')
-                    input(type='checkbox' ng-model='paragraph.lazy')
-                    span Lazy result set
-
-mixin query-actions
-    button.btn.btn-primary(ng-disabled='!queryAvailable(paragraph)' ng-click='execute(paragraph)')
-        div
-            i.fa.fa-fw.fa-play(ng-hide='paragraph.executionInProgress(false)')
-            i.fa.fa-fw.fa-refresh.fa-spin(ng-show='paragraph.executionInProgress(false)')
-            span.tipLabelExecute Execute
-    button.btn.btn-primary(ng-disabled='!queryAvailable(paragraph)' ng-click='execute(paragraph, true)')
-        div
-            i.fa.fa-fw.fa-play(ng-hide='paragraph.executionInProgress(true)')
-            i.fa.fa-fw.fa-refresh.fa-spin(ng-show='paragraph.executionInProgress(true)')
-            span.tipLabelExecute Execute on selected node
-
-
-    a.btn.btn-default(ng-disabled='!queryAvailable(paragraph)' ng-click='explain(paragraph)' data-placement='bottom' bs-tooltip='' data-title='{{queryTooltip(paragraph, "explain query")}}') Explain
-
-mixin table-result-heading-query
-    .total.row
-        .col-xs-7
-            grid-column-selector(grid-api='paragraph.gridOptions.api')
-                .fa.fa-bars.icon
-            label Page: #[b {{paragraph.page}}]
-            label.margin-left-dflt Results so far: #[b {{paragraph.rows.length + paragraph.total}}]
-            label.margin-left-dflt Duration: #[b {{paragraph.duration | duration}}]
-            label.margin-left-dflt(ng-show='paragraph.localQueryMode') NodeID8: #[b {{paragraph.resNodeId | id8}}]
-        .col-xs-2
-            div(ng-if='paragraph.qryType === "query"')
-                +result-toolbar
-        .col-xs-3
-            .pull-right
-                .btn-group.panel-tip-container
-                    button.btn.btn-primary.btn--with-icon(
-                        ng-click='exportCsv(paragraph)'
-
-                        ng-disabled='paragraph.loading'
-
-                        bs-tooltip=''
-                        ng-attr-title='{{ queryTooltip(paragraph, "export query results") }}'
-
-                        data-trigger='hover'
-                        data-placement='bottom'
-                    )
-                        svg(ignite-icon='csv' ng-if='!paragraph.csvIsPreparing')
-                        i.fa.fa-fw.fa-refresh.fa-spin(ng-if='paragraph.csvIsPreparing')
-                        span Export
-
-                    -var options = [{ text: 'Export', click: 'exportCsv(paragraph)' }, { text: 'Export all', click: 'exportCsvAll(paragraph)' }, { divider: true }, { text: '<span title="Copy current result page to clipboard">Copy to clipboard</span>', click: 'exportCsvToClipBoard(paragraph)' }]
-                    button.btn.dropdown-toggle.btn-primary(
-                        ng-disabled='paragraph.loading'
-
-                        bs-dropdown=`${JSON.stringify(options)}`
-
-                        data-toggle='dropdown'
-                        data-container='body'
-                        data-placement='bottom-right'
-                        data-html='true'
-                    )
-                        span.caret
-
-
-
-mixin table-result-heading-scan
-    .total.row
-        .col-xs-7
-            grid-column-selector(grid-api='paragraph.gridOptions.api')
-                .fa.fa-bars.icon
-            label Page: #[b {{paragraph.page}}]
-            label.margin-left-dflt Results so far: #[b {{paragraph.rows.length + paragraph.total}}]
-            label.margin-left-dflt Duration: #[b {{paragraph.duration | duration}}]
-            label.margin-left-dflt(ng-show='paragraph.localQueryMode') NodeID8: #[b {{paragraph.resNodeId | id8}}]
-        .col-xs-2
-            div(ng-if='paragraph.qryType === "query"')
-                +result-toolbar
-        .col-xs-3
-            .pull-right
-                .btn-group.panel-tip-container
-                    // TODO: replace this logic for exporting under one component
-                    button.btn.btn-primary.btn--with-icon(
-                        ng-click='exportCsv(paragraph)'
-
-                        ng-disabled='paragraph.loading || paragraph.csvIsPreparing'
-
-                        bs-tooltip=''
-                        ng-attr-title='{{ scanTooltip(paragraph) }}'
-
-                        data-trigger='hover'
-                        data-placement='bottom'
-                    )
-                        svg(ignite-icon='csv' ng-if='!paragraph.csvIsPreparing')
-                        i.fa.fa-fw.fa-refresh.fa-spin(ng-if='paragraph.csvIsPreparing')
-                        span Export
-
-                    -var options = [{ text: "Export", click: 'exportCsv(paragraph)' }, { text: 'Export all', click: 'exportCsvAll(paragraph)' }, { divider: true }, { text: '<span title="Copy current result page to clipboard">Copy to clipboard</span>', click: 'exportCsvToClipBoard(paragraph)' }]
-                    button.btn.dropdown-toggle.btn-primary(
-                        ng-disabled='paragraph.loading || paragraph.csvIsPreparing'
-
-                        bs-dropdown=`${JSON.stringify(options)}`
-
-                        data-toggle='dropdown'
-                        data-container='body'
-                        data-placement='bottom-right'
-                        data-html='true'
-                    )
-                        span.caret
-
-mixin table-result-body
-    .grid(ui-grid='paragraph.gridOptions' ui-grid-resize-columns ui-grid-exporter)
-
-mixin chart-result
-    div(ng-hide='paragraph.scanExplain()')
-        +chart-settings
-        .empty(ng-show='paragraph.chartColumns.length > 0 && !paragraph.chartColumnsConfigured()') Cannot display chart. Please configure axis using #[b Chart settings]
-        .empty(ng-show='paragraph.chartColumns.length == 0') Cannot display chart. Result set must contain Java build-in type columns. Please change query and execute it again.
-        div(ng-show='paragraph.chartColumnsConfigured()')
-            div(ng-show='paragraph.timeLineSupported() || !paragraph.chartTimeLineEnabled()')
-                div(ng-repeat='chart in paragraph.charts')
-                    nvd3(options='chart.options' data='chart.data' api='chart.api')
-            .empty(ng-show='!paragraph.timeLineSupported() && paragraph.chartTimeLineEnabled()') Pie chart does not support 'TIME_LINE' column for X-axis. Please use another column for X-axis or switch to another chart.
-    .empty(ng-show='paragraph.scanExplain()')
-        .row
-            .col-xs-4.col-xs-offset-4
-                +result-toolbar
-        label.margin-top-dflt Charts do not support #[b Explain] and #[b Scan] query
-
-mixin paragraph-scan
-    .panel-heading(bs-collapse-toggle)
-        .row
-            +paragraph-rename
-    .panel-collapse(role='tabpanel' bs-collapse-target)
-        .col-sm-12.sql-controls
-            .col-sm-3
-                +dropdown-required('Cache:', 'paragraph.cacheName', '"cache"', 'true', 'false', 'Choose cache', 'caches')
-            .col-sm-3
-                +text-enabled('Filter:', 'paragraph.filter', '"filter"', true, false, 'Enter filter')
-                    label.btn.btn-default.ignite-form-field__btn(ng-click='paragraph.caseSensitive = !paragraph.caseSensitive')
-                        input(type='checkbox' ng-model='paragraph.caseSensitive')
-                        span(bs-tooltip data-title='Select this checkbox for case sensitive search') Cs
-            label.tipLabel(bs-tooltip data-placement='bottom' data-title='Max number of rows to show in query result as one page') Page size:
-                button.btn.btn-default.select-toggle.tipLabel(ng-model='paragraph.pageSize' bs-select bs-options='item for item in pageSizes')
-
-        .col-sm-12.sql-controls
-            button.btn.btn-primary(ng-disabled='!scanAvailable(paragraph)' ng-click='scan(paragraph)')
-                div
-                    i.fa.fa-fw.fa-play(ng-hide='paragraph.checkScanInProgress(false)')
-                    i.fa.fa-fw.fa-refresh.fa-spin(ng-show='paragraph.checkScanInProgress(false)')
-                    span.tipLabelExecute Scan
-
-            button.btn.btn-primary(ng-disabled='!scanAvailable(paragraph)' ng-click='scan(paragraph, true)')
-                    i.fa.fa-fw.fa-play(ng-hide='paragraph.checkScanInProgress(true)')
-                    i.fa.fa-fw.fa-refresh.fa-spin(ng-show='paragraph.checkScanInProgress(true)')
-                    span.tipLabelExecute Scan on selected node
-
-        .col-sm-12.sql-result(ng-if='paragraph.queryExecuted()' ng-switch='paragraph.resultType()')
-            .error(ng-switch-when='error') Error: {{paragraph.error.message}}
-            .empty(ng-switch-when='empty') Result set is empty. Duration: #[b {{paragraph.duration | duration}}]
-            .table(ng-switch-when='table')
-                +table-result-heading-scan
-                +table-result-body
-            .footer.clearfix()
-                .pull-left
-                    | Showing results for scan of #[b {{ paragraph.queryArgs.cacheName | defaultName }}]
-                    span(ng-if='paragraph.queryArgs.filter') &nbsp; with filter: #[b {{ paragraph.queryArgs.filter }}]
-                    span(ng-if='paragraph.queryArgs.localNid') &nbsp; on node: #[b {{ paragraph.queryArgs.localNid | limitTo:8 }}]
-
-                -var nextVisibleCondition = 'paragraph.resultType() != "error" && paragraph.queryId && paragraph.nonRefresh() && (paragraph.table() || paragraph.chart() && !paragraph.scanExplain())'
-
-                .pull-right(ng-show=`${nextVisibleCondition}` ng-class='{disabled: paragraph.loading}' ng-click='!paragraph.loading && nextPage(paragraph)')
-                    i.fa.fa-chevron-circle-right
-                    a Next
-
-mixin paragraph-query
-    .row.panel-heading(bs-collapse-toggle)
-        +paragraph-rename
-    .panel-collapse(role='tabpanel' bs-collapse-target)
-        .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"],' +
-                'advanced: {enableSnippets: false, enableBasicAutocompletion: true, enableLiveAutocompletion: true}}'
-                ng-model='paragraph.query')
-            .col-xs-4.col-sm-3
-                div(ng-show='caches.length > 0' style='padding: 5px 10px' st-table='displayedCaches' st-safe-src='caches')
-                    lable.labelField.labelFormField Caches:
-                    i.fa.fa-database.tipField(title='Click to show cache types metadata dialog' bs-popover data-template-url='{{ $ctrl.cacheMetadataTemplateUrl }}' data-placement='bottom' data-trigger='click' data-container='#{{ paragraph.id }}')
-                    .input-tip
-                        input.form-control(type='text' st-search='label' placeholder='Filter caches...')
-                    table.links
-                        tbody.scrollable-y(style='max-height: 15em; display: block;')
-                            tr(ng-repeat='cache in displayedCaches track by cache.name')
-                                td(style='width: 100%')
-                                    input.labelField(id='cache_{{ [paragraph.id, $index].join("_") }}' type='radio' value='{{cache.name}}' ng-model='paragraph.cacheName')
-                                    label(for='cache_{{ [paragraph.id, $index].join("_") }} ' ng-bind-html='cache.label')
-                    .settings-row
-                        .row(ng-if='ddlAvailable(paragraph)')
-                            label.tipLabel.use-cache(bs-tooltip data-placement='bottom'
-                                data-title=
-                                    'Use selected cache as default schema name.<br/>\
-                                    This will allow to execute query on specified cache without specify schema name.<br/>\
-                                    <b>NOTE:</b> In future version of Ignite this feature will be removed.'
-                                data-trigger='hover')
-                                input(type='checkbox' ng-model='paragraph.useAsDefaultSchema')
-                                span Use selected cache as default schema name
-                .empty-caches(ng-show='displayedCaches.length == 0 && caches.length != 0')
-                    label Wrong caches filter
-                .empty-caches(ng-show='caches.length == 0')
-                    label No caches
-        .col-sm-12.sql-controls
-            +query-actions
-
-            .pull-right
-                +query-settings
-        .col-sm-12.sql-result(ng-if='paragraph.queryExecuted()' ng-switch='paragraph.resultType()')
-            .error(ng-switch-when='error')
-                label Error: {{paragraph.error.message}}
-                br
-                a(ng-show='paragraph.resultType() === "error"' ng-click='showStackTrace(paragraph)') Show more
-            .empty(ng-switch-when='empty') Result set is empty. Duration: #[b {{paragraph.duration | duration}}]
-            .table(ng-switch-when='table')
-                +table-result-heading-query
-                +table-result-body
-            .chart(ng-switch-when='chart')
-                +chart-result
-            .footer.clearfix(ng-show='paragraph.resultType() !== "error"')
-                a.pull-left(ng-click='showResultQuery(paragraph)') Show query
-
-                -var nextVisibleCondition = 'paragraph.resultType() !== "error" && paragraph.queryId && paragraph.nonRefresh() && (paragraph.table() || paragraph.chart() && !paragraph.scanExplain())'
-
-                .pull-right(ng-show=`${nextVisibleCondition}` ng-class='{disabled: paragraph.loading}' ng-click='!paragraph.loading && nextPage(paragraph)')
-                    i.fa.fa-chevron-circle-right
-                    a Next
-
-.row
-    .docs-content
-        .row(ng-if='notebook' bs-affix style='margin-bottom: 20px;')
-            +notebook-rename
-
-        ignite-information(data-title='With query notebook you can' style='margin-top: 0; margin-bottom: 30px')
-            ul
-                li Create any number of queries
-                li Execute and explain SQL queries
-                li Execute scan queries
-                li View data in tabular form and as charts
-
-        div(ng-if='notebookLoadFailed' style='text-align: center')
-            +notebook-error
-
-        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-paragraph(ng-repeat='paragraph in notebook.paragraphs' id='{{paragraph.id}}' ng-form='form_{{paragraph.id}}')
-                        .panel.panel-default(ng-if='paragraph.qryType === "scan"')
-                            +paragraph-scan
-                        .panel.panel-default(ng-if='paragraph.qryType === "query"')
-                            +paragraph-query

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/ClusterListener.java
----------------------------------------------------------------------
diff --git a/modules/web-console/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/ClusterListener.java b/modules/web-console/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/ClusterListener.java
index 8eed3dd..86b9ea5 100644
--- a/modules/web-console/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/ClusterListener.java
+++ b/modules/web-console/web-agent/src/main/java/org/apache/ignite/console/agent/handlers/ClusterListener.java
@@ -22,44 +22,51 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import io.socket.client.Socket;
 import io.socket.emitter.Emitter;
 import java.net.ConnectException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
+import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.console.agent.rest.RestExecutor;
 import org.apache.ignite.console.agent.rest.RestResult;
 import org.apache.ignite.internal.processors.rest.client.message.GridClientNodeBean;
 import org.apache.ignite.internal.processors.rest.protocols.http.jetty.GridJettyObjectMapper;
 import org.apache.ignite.internal.util.typedef.F;
-import org.apache.ignite.internal.util.typedef.T2;
+import org.apache.ignite.internal.util.typedef.internal.LT;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteClosure;
 import org.apache.ignite.lang.IgniteProductVersion;
-import org.slf4j.Logger;
+import org.apache.ignite.logger.slf4j.Slf4jLogger;
 import org.slf4j.LoggerFactory;
 
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_CLUSTER_NAME;
 import static org.apache.ignite.console.agent.AgentUtils.toJSON;
 import static org.apache.ignite.internal.IgniteNodeAttributes.ATTR_BUILD_VER;
+import static org.apache.ignite.internal.IgniteNodeAttributes.ATTR_CLIENT_MODE;
+import static org.apache.ignite.internal.IgniteNodeAttributes.ATTR_IPS;
 import static org.apache.ignite.internal.processors.rest.GridRestResponse.STATUS_SUCCESS;
+import static org.apache.ignite.internal.visor.util.VisorTaskUtils.sortAddresses;
+import static org.apache.ignite.internal.visor.util.VisorTaskUtils.splitAddresses;
 
 /**
  * API to transfer topology from Ignite cluster available by node-uri.
  */
 public class ClusterListener {
     /** */
-    private static final Logger log = LoggerFactory.getLogger(ClusterListener.class);
+    private static final IgniteLogger log = new Slf4jLogger(LoggerFactory.getLogger(ClusterListener.class));
 
     /** */
     private static final String EVENT_CLUSTER_CONNECTED = "cluster:connected";
 
     /** */
     private static final String EVENT_CLUSTER_TOPOLOGY = "cluster:topology";
-    
+
     /** */
     private static final String EVENT_CLUSTER_DISCONNECTED = "cluster:disconnected";
 
@@ -79,17 +86,6 @@ public class ClusterListener {
     private final BroadcastTask broadcastTask = new BroadcastTask();
 
     /** */
-    private static final IgniteClosure<GridClientNodeBean, UUID> NODE2ID = new IgniteClosure<GridClientNodeBean, UUID>() {
-        @Override public UUID apply(GridClientNodeBean n) {
-            return n.getNodeId();
-        }
-
-        @Override public String toString() {
-            return "Node bean to node ID transformer closure.";
-        }
-    };
-
-    /** */
     private static final IgniteClosure<UUID, String> ID2ID8 = new IgniteClosure<UUID, String>() {
         @Override public String apply(UUID nid) {
             return U.id8(nid).toUpperCase();
@@ -127,7 +123,7 @@ public class ClusterListener {
      * @param nids Cluster nodes IDs.
      */
     private void clusterConnect(Collection<UUID> nids) {
-        log.info("Connection successfully established to cluster with nodes: {}", F.viewReadOnly(nids, ID2ID8));
+        log.info("Connection successfully established to cluster with nodes: " + F.viewReadOnly(nids, ID2ID8));
 
         client.emit(EVENT_CLUSTER_CONNECTED, toJSON(nids));
     }
@@ -171,7 +167,7 @@ public class ClusterListener {
             @Override public void call(Object... args) {
                 safeStopRefresh();
 
-                final long timeout = args.length > 1  && args[1] instanceof Long ? (long)args[1] : DFLT_TIMEOUT;
+                final long timeout = args.length > 1 && args[1] instanceof Long ? (long)args[1] : DFLT_TIMEOUT;
 
                 refreshTask = pool.scheduleWithFixedDelay(broadcastTask, 0L, timeout, TimeUnit.MILLISECONDS);
             }
@@ -194,41 +190,107 @@ public class ClusterListener {
     /** */
     private static class TopologySnapshot {
         /** */
+        private String clusterName;
+
+        /** */
         private Collection<UUID> nids;
 
         /** */
-        private String clusterVer;
+        private Map<UUID, String> addrs;
+
+        /** */
+        private Map<UUID, Boolean> clients;
+
+        /** */
+        private String clusterVerStr;
+
+        /** */
+        private IgniteProductVersion clusterVer;
+
+        /** */
+        private boolean active;
+
+        /**
+         * Helper method to get attribute.
+         *
+         * @param attrs Map with attributes.
+         * @param name Attribute name.
+         * @return Attribute value.
+         */
+        private static <T> T attribute(Map<String, Object> attrs, String name) {
+            return (T)attrs.get(name);
+        }
 
         /**
          * @param nodes Nodes.
          */
         TopologySnapshot(Collection<GridClientNodeBean> nodes) {
-            nids = F.viewReadOnly(nodes, NODE2ID);
+            int sz = nodes.size();
+
+            nids = new ArrayList<>(sz);
+            addrs = U.newHashMap(sz);
+            clients = U.newHashMap(sz);
+            active = false;
+
+            for (GridClientNodeBean node : nodes) {
+                UUID nid = node.getNodeId();
+
+                nids.add(nid);
 
-            Collection<T2<String, IgniteProductVersion>> vers = F.transform(nodes,
-                new IgniteClosure<GridClientNodeBean, T2<String, IgniteProductVersion>>() {
-                    @Override public T2<String, IgniteProductVersion> apply(GridClientNodeBean bean) {
-                        String ver = (String)bean.getAttributes().get(ATTR_BUILD_VER);
+                Map<String, Object> attrs = node.getAttributes();
 
-                        return new T2<>(ver, IgniteProductVersion.fromString(ver));
-                    }
-                });
+                if (F.isEmpty(clusterName))
+                    clusterName = attribute(attrs, IGNITE_CLUSTER_NAME);
 
-            T2<String, IgniteProductVersion> min = Collections.min(vers, new Comparator<T2<String, IgniteProductVersion>>() {
-                @SuppressWarnings("ConstantConditions")
-                @Override public int compare(T2<String, IgniteProductVersion> o1, T2<String, IgniteProductVersion> o2) {
-                    return o1.get2().compareTo(o2.get2());
+                Boolean client = attribute(attrs, ATTR_CLIENT_MODE);
+
+                clients.put(nid, client);
+
+                Collection<String> nodeAddrs = client
+                    ? splitAddresses((String)attribute(attrs, ATTR_IPS))
+                    : node.getTcpAddresses();
+
+                String firstIP = F.first(sortAddresses(nodeAddrs));
+
+                addrs.put(nid, firstIP);
+
+                String nodeVerStr = attribute(attrs, ATTR_BUILD_VER);
+
+                IgniteProductVersion nodeVer = IgniteProductVersion.fromString(nodeVerStr);
+
+                if (clusterVer == null || clusterVer.compareTo(nodeVer) > 0) {
+                    clusterVer = nodeVer;
+                    clusterVerStr = nodeVerStr;
                 }
-            });
+            }
+        }
 
-            clusterVer = min.get1();
+        /**
+         * @return Cluster name.
+         */
+        public String getClusterName() {
+            return clusterName;
         }
 
         /**
          * @return Cluster version.
          */
         public String getClusterVersion() {
-            return clusterVer;
+            return clusterVerStr;
+        }
+
+        /**
+         * @return Cluster active flag.
+         */
+        public boolean isActive() {
+            return active;
+        }
+
+        /**
+         * @param active New cluster active state.
+         */
+        public void setActive(boolean active) {
+            this.active = active;
         }
 
         /**
@@ -238,14 +300,40 @@ public class ClusterListener {
             return nids;
         }
 
-        /**  */
+        /**
+         * @return Cluster nodes with IPs.
+         */
+        public Map<UUID, String> getAddresses() {
+            return addrs;
+        }
+
+        /**
+         * @return Cluster nodes with client mode flag.
+         */
+        public Map<UUID, Boolean> getClients() {
+            return clients;
+        }
+
+        /**
+         * @return Cluster version.
+         */
+        public IgniteProductVersion clusterVersion() {
+            return clusterVer;
+        }
+
+        /**
+         * @return Collection of short UUIDs.
+         */
         Collection<String> nid8() {
             return F.viewReadOnly(nids, ID2ID8);
         }
 
-        /**  */
-        boolean differentCluster(TopologySnapshot old) {
-            return old == null || F.isEmpty(old.nids) || Collections.disjoint(nids, old.nids);
+        /**
+         * @param prev Previous topology.
+         * @return {@code true} in case if current topology is a new cluster.
+         */
+        boolean differentCluster(TopologySnapshot prev) {
+            return prev == null || F.isEmpty(prev.nids) || Collections.disjoint(nids, prev.nids);
         }
     }
 
@@ -264,7 +352,11 @@ public class ClusterListener {
                         TopologySnapshot newTop = new TopologySnapshot(nodes);
 
                         if (newTop.differentCluster(top))
-                            log.info("Connection successfully established to cluster with nodes: {}", newTop.nid8());
+                            log.info("Connection successfully established to cluster with nodes: " + newTop.nid8());
+
+                        boolean active = restExecutor.active(newTop.clusterVersion(), F.first(newTop.getNids()));
+
+                        newTop.setActive(active);
 
                         top = newTop;
 
@@ -273,7 +365,7 @@ public class ClusterListener {
                         break;
 
                     default:
-                        log.warn(res.getError());
+                        LT.warn(log, res.getError());
 
                         clusterDisconnect();
                 }
@@ -288,7 +380,7 @@ public class ClusterListener {
             }
         }
     }
-    
+
     /** */
     private class BroadcastTask implements Runnable {
         /** {@inheritDoc} */
@@ -306,7 +398,7 @@ public class ClusterListener {
                         if (top.differentCluster(newTop)) {
                             clusterDisconnect();
 
-                            log.info("Connection successfully established to cluster with nodes: {}", newTop.nid8());
+                            log.info("Connection successfully established to cluster with nodes: " + newTop.nid8());
 
                             watch();
                         }
@@ -318,7 +410,7 @@ public class ClusterListener {
                         break;
 
                     default:
-                        log.warn(res.getError());
+                        LT.warn(log, res.getError());
 
                         clusterDisconnect();
                 }

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/web-agent/src/main/java/org/apache/ignite/console/agent/rest/RestExecutor.java
----------------------------------------------------------------------
diff --git a/modules/web-console/web-agent/src/main/java/org/apache/ignite/console/agent/rest/RestExecutor.java b/modules/web-console/web-agent/src/main/java/org/apache/ignite/console/agent/rest/RestExecutor.java
index 36f3885..7fbe6f9 100644
--- a/modules/web-console/web-agent/src/main/java/org/apache/ignite/console/agent/rest/RestExecutor.java
+++ b/modules/web-console/web-agent/src/main/java/org/apache/ignite/console/agent/rest/RestExecutor.java
@@ -30,6 +30,7 @@ import java.io.StringWriter;
 import java.net.ConnectException;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 import okhttp3.Dispatcher;
 import okhttp3.FormBody;
@@ -44,6 +45,7 @@ import org.apache.ignite.console.demo.AgentClusterDemo;
 import org.apache.ignite.internal.processors.rest.protocols.http.jetty.GridJettyObjectMapper;
 import org.apache.ignite.internal.util.typedef.internal.LT;
 import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.lang.IgniteProductVersion;
 import org.apache.ignite.logger.slf4j.Slf4jLogger;
 import org.slf4j.LoggerFactory;
 
@@ -59,6 +61,18 @@ import static org.apache.ignite.internal.processors.rest.GridRestResponse.STATUS
  */
 public class RestExecutor {
     /** */
+    private static final IgniteProductVersion IGNITE_2_1 = IgniteProductVersion.fromString("2.1.0");
+
+    /** */
+    private static final IgniteProductVersion IGNITE_2_3 = IgniteProductVersion.fromString("2.3.0");
+
+    /** Unique Visor key to get events last order. */
+    private static final String EVT_LAST_ORDER_KEY = "WEB_AGENT_" + UUID.randomUUID().toString();
+
+    /** Unique Visor key to get events throttle counter. */
+    private static final String EVT_THROTTLE_CNTR_KEY = "WEB_AGENT_" + UUID.randomUUID().toString();
+
+    /** */
     private static final IgniteLogger log = new Slf4jLogger(LoggerFactory.getLogger(RestExecutor.class));
 
     /** JSON object mapper. */
@@ -208,7 +222,9 @@ public class RestExecutor {
     }
 
     /**
-     * @param demo Is demo node request.
+     * @param demo {@code true} in case of demo mode.
+     * @param full Flag indicating whether to collect metrics or not.
+     * @throws IOException If failed to collect topology.
      */
     public RestResult topology(boolean demo, boolean full) throws IOException {
         Map<String, Object> params = new HashMap<>(3);
@@ -221,6 +237,51 @@ public class RestExecutor {
     }
 
     /**
+     * @param ver Cluster version.
+     * @param nid Node ID.
+     * @return Cluster active state.
+     * @throws IOException If failed to collect cluster active state.
+     */
+    public boolean active(IgniteProductVersion ver, UUID nid) throws IOException {
+        Map<String, Object> params = new HashMap<>();
+
+        boolean v23 = ver.compareTo(IGNITE_2_3) >= 0;
+
+        if (v23)
+            params.put("cmd", "currentState");
+        else {
+            params.put("cmd", "exe");
+            params.put("name", "org.apache.ignite.internal.visor.compute.VisorGatewayTask");
+            params.put("p1", nid);
+            params.put("p2", "org.apache.ignite.internal.visor.node.VisorNodeDataCollectorTask");
+            params.put("p3", "org.apache.ignite.internal.visor.node.VisorNodeDataCollectorTaskArg");
+            params.put("p4", false);
+            params.put("p5", EVT_LAST_ORDER_KEY);
+            params.put("p6", EVT_THROTTLE_CNTR_KEY);
+
+            if (ver.compareTo(IGNITE_2_1) >= 0)
+                params.put("p7", false);
+            else {
+                params.put("p7", 10);
+                params.put("p8", false);
+            }
+        }
+
+        RestResult res = sendRequest(false, "ignite", params, null, null);
+
+        switch (res.getStatus()) {
+            case STATUS_SUCCESS:
+                if (v23)
+                    return Boolean.valueOf(res.getData());
+
+                return res.getData().contains("\"active\":true");
+
+            default:
+                throw new IOException(res.getError());
+        }
+    }
+
+    /**
      * REST response holder Java bean.
      */
     private static class RestResponseHolder {


[4/5] ignite git commit: IGNITE-6390 Web Console: Added component for cluster selection.

Posted by ak...@apache.org.
http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/page-queries/controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-queries/controller.js b/modules/web-console/frontend/app/components/page-queries/controller.js
new file mode 100644
index 0000000..dba0269
--- /dev/null
+++ b/modules/web-console/frontend/app/components/page-queries/controller.js
@@ -0,0 +1,1938 @@
+/*
+ * 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 '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 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';
+
+// 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 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 {
+    constructor($animate, $timeout, JavaTypes, 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 = err.message;
+
+            let cause = err;
+
+            while (_.nonNil(cause)) {
+                if (_.nonEmpty(cause.className) &&
+                    _.includes(['SQLException', 'JdbcSQLException', 'QueryCancelledException'], JavaTypes.shortClassName(cause.className))) {
+                    this.error.message = 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 default class {
+    static $inject = ['$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'];
+
+    constructor($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, IgniteCopyToClipboard) {
+        const $ctrl = this;
+
+        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 });
+
+        // Define template urls.
+        $ctrl.paragraphRateTemplateUrl = paragraphRateTemplateUrl;
+        $ctrl.cacheMetadataTemplateUrl = cacheMetadataTemplateUrl;
+        $ctrl.chartSettingsTemplateUrl = chartSettingsTemplateUrl;
+        $ctrl.demoStarted = false;
+
+        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.pageSizes = [50, 100, 200, 400, 800, 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: -25}
+                        }
+                    }
+                };
+
+                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: -25
+                                }
+                            }
+                        },
+                        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: -25
+                            }
+                        }
+                    }
+                };
+
+                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: -25}
+                        }
+                    }
+                };
+
+                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 _refreshFn = () => {
+            return agentMgr.topology(true)
+                .then((nodes) => {
+                    $scope.caches = _.sortBy(_.reduce(nodes, (cachesAcc, node) => {
+                        _.forEach(node.caches, (cache) => {
+                            let item = _.find(cachesAcc, {name: cache.name});
+
+                            if (_.isNil(item)) {
+                                cache.label = maskCacheName(cache.name, true);
+                                cache.value = cache.name;
+
+                                cache.nodes = [];
+
+                                cachesAcc.push(item = cache);
+                            }
+
+                            item.nodes.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 cachesAcc;
+                    }, []), (cache) => cache.label.toLowerCase());
+
+                    // Reset to first cache in case of stopped selected.
+                    const cacheNames = _.map($scope.caches, (cache) => cache.value);
+
+                    _.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));
+                    }
+                })
+                .catch((err) => Messages.showError(err));
+        };
+
+        const _startWatch = () => {
+            const awaitClusters$ = fromPromise(
+                agentMgr.startClusterWatch('Back to Configuration', 'base.configuration.tabs.advanced.clusters'));
+
+            const currentCluster$ = agentMgr.connectionSbj
+                .distinctUntilChanged((n, o) => n.cluster === o.cluster);
+
+            const finishLoading$ = defer(() => {
+                if (!$root.IgniteDemoMode)
+                    Loading.finish('sqlLoading');
+            }).take(1);
+
+            const refreshCaches = (period) => {
+                return timer(0, period).exhaustMap(() => _refreshFn()).merge(finishLoading$);
+            };
+
+            this.refresh$ = awaitClusters$
+                .mergeMap(() => currentCluster$)
+                .do(() => Loading.start('sqlLoading'))
+                .do(() => {
+                    _.forEach($scope.notebook.paragraphs, (paragraph) => {
+                        paragraph.reset($interval);
+                    });
+                })
+                .switchMap(() => refreshCaches(5000))
+                .subscribe();
+        };
+
+        Notebook.find($state.params.noteId)
+            .then((notebook) => {
+                $scope.notebook = _.cloneDeep(notebook);
+
+                $scope.notebook_name = $scope.notebook.name;
+
+                if (!$scope.notebook.expandedParagraphs)
+                    $scope.notebook.expandedParagraphs = [];
+
+                if (!$scope.notebook.paragraphs)
+                    $scope.notebook.paragraphs = [];
+
+                $scope.notebook.paragraphs = _.map($scope.notebook.paragraphs,
+                    (paragraph) => new Paragraph($animate, $timeout, JavaTypes, paragraph));
+
+                if (_.isEmpty($scope.notebook.paragraphs))
+                    $scope.addQuery();
+                else
+                    $scope.rebuildScrollParagraphs();
+            })
+            .then(() => _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.renameParagraph = function(paragraph, newName) {
+            if (!newName)
+                return;
+
+            if (paragraph.name !== newName) {
+                paragraph.name = newName;
+
+                $scope.rebuildScrollParagraphs();
+
+                Notebook.save($scope.notebook)
+                    .then(() => paragraph.edit = false)
+                    .catch(Messages.showError);
+            }
+            else
+                paragraph.edit = false;
+        };
+
+        $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({ action: '/queries/add/query' });
+
+            const paragraph = new Paragraph($animate, $timeout, JavaTypes, {
+                name: 'Query' + (sz === 0 ? '' : sz),
+                query: '',
+                pageSize: $scope.pageSizes[1],
+                timeLineSpan: $scope.timeLineSpans[0],
+                result: 'none',
+                rate: {
+                    value: 1,
+                    unit: 60000,
+                    installed: false
+                },
+                qryType: 'query'
+            });
+
+            $scope.addParagraph(paragraph, sz);
+
+            $timeout(() => {
+                $anchorScroll();
+
+                paragraph.ace.focus();
+            });
+        };
+
+        $scope.addScan = function() {
+            const sz = $scope.notebook.paragraphs.length;
+
+            ActivitiesData.post({ action: '/queries/add/scan' });
+
+            const paragraph = new Paragraph($animate, $timeout, JavaTypes, {
+                name: 'Scan' + (sz === 0 ? '' : sz),
+                query: '',
+                pageSize: $scope.pageSizes[1],
+                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.removeParagraph = function(paragraph) {
+            Confirm.confirm('Are you sure you want to remove query: "' + paragraph.name + '"?')
+                .then(function() {
+                    $scope.stopRefresh(paragraph);
+
+                    const paragraph_idx = _.findIndex($scope.notebook.paragraphs, function(item) {
+                        return paragraph === item;
+                    });
+
+                    const panel_idx = _.findIndex($scope.expandedParagraphs, function(item) {
+                        return paragraph_idx === item;
+                    });
+
+                    if (panel_idx >= 0)
+                        $scope.expandedParagraphs.splice(panel_idx, 1);
+
+                    $scope.notebook.paragraphs.splice(paragraph_idx, 1);
+
+                    $scope.rebuildScrollParagraphs();
+
+                    Notebook.save($scope.notebook)
+                        .catch(Messages.showError);
+                });
+        };
+
+        $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.
+         * @return {Array.<String>} Nids
+         */
+        const cacheNodes = (name) => {
+            return _.find($scope.caches, {name}).nodes;
+        };
+
+        /**
+         * @param {String} name Cache name.
+         * @param {Boolean} local Local query.
+         * @return {String} Nid
+         */
+        const _chooseNode = (name, local) => {
+            if (_.isEmpty(name))
+                return Promise.resolve(null);
+
+            const nodes = _.filter(cacheNodes(name), (node) => !node.client);
+
+            if (local) {
+                return Nodes.selectNode(nodes, name)
+                    .then((selectedNids) => _.head(selectedNids));
+            }
+
+            return Promise.resolve(nodes[_.random(0, nodes.length - 1)].nid);
+        };
+
+        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))
+                .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 = (paragraph) => {
+            const cache = _.find($scope.caches, {name: paragraph.cacheName});
+
+            if (cache)
+                return !!_.find(cache.nodes, (node) => Version.since(node.version, NON_COLLOCATED_JOINS_SINCE));
+
+            return false;
+        };
+
+        $scope.enforceJoinOrderAvailable = (paragraph) => {
+            const cache = _.find($scope.caches, {name: paragraph.cacheName});
+
+            if (cache)
+                return !!_.find(cache.nodes, (node) => Version.since(node.version, ...ENFORCE_JOIN_SINCE));
+
+            return false;
+        };
+
+        $scope.lazyQueryAvailable = (paragraph) => {
+            const cache = _.find($scope.caches, {name: paragraph.cacheName});
+
+            if (cache)
+                return !!_.find(cache.nodes, (node) => Version.since(node.version, ...LAZY_QUERY_SINCE));
+
+            return false;
+        };
+
+        $scope.ddlAvailable = (paragraph) => {
+            const cache = _.find($scope.caches, {name: paragraph.cacheName});
+
+            if (cache)
+                return !!_.find(cache.nodes, (node) => Version.since(node.version, ...DDL_SINCE));
+
+            return false;
+        };
+
+        $scope.execute = (paragraph, local = false) => {
+            const nonCollocatedJoins = !!paragraph.nonCollocatedJoins;
+            const enforceJoinOrder = !!paragraph.enforceJoinOrder;
+            const lazy = !!paragraph.lazy;
+
+            $scope.queryAvailable(paragraph) && _chooseNode(paragraph.cacheName, local)
+                .then((nid) => {
+                    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 args = paragraph.queryArgs = {
+                                type: 'QUERY',
+                                cacheName: ($scope.ddlAvailable(paragraph) && !paragraph.useAsDefaultSchema) ? null : paragraph.cacheName,
+                                query: paragraph.query,
+                                pageSize: paragraph.pageSize,
+                                maxPages: paragraph.maxPages,
+                                nonCollocatedJoins,
+                                enforceJoinOrder,
+                                localNid: local ? nid : null,
+                                lazy
+                            };
+
+                            const qry = args.maxPages ? addLimit(args.query, args.pageSize * args.maxPages) : paragraph.query;
+
+                            ActivitiesData.post({ action: '/queries/execute' });
+
+                            return agentMgr.querySql(nid, args.cacheName, qry, nonCollocatedJoins, enforceJoinOrder, false, local, args.pageSize, lazy);
+                        })
+                        .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) => {
+            if (!$scope.queryAvailable(paragraph))
+                return;
+
+            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: paragraph.cacheName,
+                        query: 'EXPLAIN ' + paragraph.query,
+                        pageSize: paragraph.pageSize
+                    };
+
+                    ActivitiesData.post({ action: '/queries/explain' });
+
+                    return agentMgr.querySql(nid, args.cacheName, args.query, false, !!paragraph.enforceJoinOrder, false, false, args.pageSize, false);
+                })
+                .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;
+
+            paragraph.localQueryMode = local;
+
+            $scope.scanAvailable(paragraph) && _chooseNode(cacheName, local)
+                .then((nid) => {
+                    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({ 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) => {
+            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(';') + '\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(';') + '\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))
+                .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, { name: 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)) {
+                        const clsName = _.isEmpty(item.className) ? '' : '[' + JavaTypes.shortClassName(item.className) + '] ';
+
+                        scope.content.push((scope.content.length > 0 ? tab : '') + clsName + (item.message || ''));
+
+                        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});
+            }
+        };
+    }
+
+    $onInit() {
+
+    }
+
+    $onDestroy() {
+        this.refresh$.unsubscribe();
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/page-queries/index.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-queries/index.js b/modules/web-console/frontend/app/components/page-queries/index.js
new file mode 100644
index 0000000..4b553eb
--- /dev/null
+++ b/modules/web-console/frontend/app/components/page-queries/index.js
@@ -0,0 +1,62 @@
+/*
+ * 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 './style.scss';
+import angular from 'angular';
+
+import templateUrl from './template.tpl.pug';
+
+import NotebookData from './Notebook.data';
+import Notebook from './Notebook.service';
+import notebook from './notebook.controller';
+import controller from './controller';
+
+export default angular.module('ignite-console.sql', [
+    'ui.router'
+])
+.component('pageQueries', {
+    controller,
+    templateUrl
+})
+.config(['$stateProvider', ($stateProvider) => {
+    // set up the states
+    $stateProvider
+        .state('base.sql', {
+            url: '/queries',
+            abstract: true,
+            template: '<ui-view></ui-view>'
+        })
+        .state('base.sql.notebook', {
+            url: '/notebook/{noteId}',
+            component: 'pageQueries',
+            permission: 'query',
+            tfMetaTags: {
+                title: 'Query notebook'
+            }
+        })
+        .state('base.sql.demo', {
+            url: '/demo',
+            component: 'pageQueries',
+            permission: 'query',
+            tfMetaTags: {
+                title: 'SQL demo'
+            }
+        });
+}])
+.service('IgniteNotebookData', NotebookData)
+.service('IgniteNotebook', Notebook)
+.controller('notebookController', notebook);

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/page-queries/notebook.controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-queries/notebook.controller.js b/modules/web-console/frontend/app/components/page-queries/notebook.controller.js
new file mode 100644
index 0000000..68d318a
--- /dev/null
+++ b/modules/web-console/frontend/app/components/page-queries/notebook.controller.js
@@ -0,0 +1,62 @@
+/*
+ * 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 templateUrl from 'views/sql/notebook-new.tpl.pug';
+
+// Controller that load notebooks in navigation bar .
+export default ['$scope', '$modal', '$state', 'IgniteMessages', 'IgniteNotebook',
+    (scope, $modal, $state, Messages, Notebook) => {
+        // Pre-fetch modal dialogs.
+        const nameModal = $modal({scope, templateUrl, show: false});
+
+        scope.create = (name) => {
+            return Notebook.create(name)
+                .then((notebook) => {
+                    nameModal.hide();
+
+                    $state.go('base.sql.notebook', {noteId: notebook._id});
+                })
+                .catch(Messages.showError);
+        };
+
+        scope.createNotebook = () => nameModal.$promise.then(nameModal.show);
+
+        Notebook.read()
+            .then((notebooks) => {
+                scope.$watchCollection(() => notebooks, (changed) => {
+                    if (_.isEmpty(changed))
+                        return scope.notebooks = [];
+
+                    scope.notebooks = [
+                        {text: 'Create new notebook', click: scope.createNotebook},
+                        {divider: true}
+                    ];
+
+                    _.forEach(changed, (notebook) => scope.notebooks.push({
+                        data: notebook,
+                        action: {
+                            icon: 'fa-trash',
+                            click: (item) => Notebook.remove(item)
+                        },
+                        text: notebook.name,
+                        sref: `base.sql.notebook({noteId:"${notebook._id}"})`
+                    }));
+                });
+            })
+            .catch(Messages.showError);
+    }
+];

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/page-queries/style.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-queries/style.scss b/modules/web-console/frontend/app/components/page-queries/style.scss
new file mode 100644
index 0000000..70136fd
--- /dev/null
+++ b/modules/web-console/frontend/app/components/page-queries/style.scss
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+page-queries {
+    .docs-content > header {
+        margin: 0;
+        margin-bottom: 30px;
+
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+
+        h1 {
+        	margin: 0;
+        	margin-right: 8px;
+        }
+    }
+
+    .affix + .block-information {
+        margin-top: 90px;
+    }
+}


[3/5] ignite git commit: IGNITE-6390 Web Console: Added component for cluster selection.

Posted by ak...@apache.org.
http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/page-queries/template.tpl.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-queries/template.tpl.pug b/modules/web-console/frontend/app/components/page-queries/template.tpl.pug
new file mode 100644
index 0000000..b2173f7
--- /dev/null
+++ b/modules/web-console/frontend/app/components/page-queries/template.tpl.pug
@@ -0,0 +1,385 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+mixin btn-toolbar(btn, click, tip, focusId)
+    i.btn.btn-default.fa(class=btn ng-click=click bs-tooltip='' data-title=tip ignite-on-click-focus=focusId data-trigger='hover' data-placement='bottom')
+
+mixin btn-toolbar-data(btn, kind, tip)
+    i.btn.btn-default.fa(class=btn ng-click=`setResult(paragraph, '${kind}')` ng-class=`{active: resultEq(paragraph, '${kind}')}` bs-tooltip='' data-title=tip data-trigger='hover' data-placement='bottom')
+
+mixin result-toolbar
+    .btn-group(ng-model='paragraph.result' ng-click='$event.stopPropagation()' style='left: 50%; margin: 0 0 0 -70px;display: block;')
+        +btn-toolbar-data('fa-table', 'table', 'Show data in tabular form')
+        +btn-toolbar-data('fa-bar-chart', 'bar', 'Show bar chart<br/>By default first column - X values, second column - Y values<br/>In case of one column it will be treated as Y values')
+        +btn-toolbar-data('fa-pie-chart', 'pie', 'Show pie chart<br/>By default first column - pie labels, second column - pie values<br/>In case of one column it will be treated as pie values')
+        +btn-toolbar-data('fa-line-chart', 'line', 'Show line chart<br/>By default first column - X values, second column - Y values<br/>In case of one column it will be treated as Y values')
+        +btn-toolbar-data('fa-area-chart', 'area', 'Show area chart<br/>By default first column - X values, second column - Y values<br/>In case of one column it will be treated as Y values')
+
+mixin chart-settings
+    .total.row
+        .col-xs-7
+            .chart-settings-link(ng-show='paragraph.chart && paragraph.chartColumns.length > 0')
+                a(title='Click to show chart settings dialog' ng-click='$event.stopPropagation()' bs-popover data-template-url='{{ $ctrl.chartSettingsTemplateUrl }}' data-placement='bottom' data-auto-close='1' data-trigger='click')
+                    i.fa.fa-bars
+                    | Chart settings
+                div(ng-show='paragraphTimeSpanVisible(paragraph)')
+                    label Show
+                    button.select-manual-caret.btn.btn-default(ng-model='paragraph.timeLineSpan' ng-change='applyChartSettings(paragraph)' bs-options='item for item in timeLineSpans' bs-select data-caret-html='<span class="caret"></span>')
+                    label min
+
+                div
+                    label Duration: #[b {{paragraph.duration | duration}}]
+                    label.margin-left-dflt(ng-show='paragraph.localQueryMode') NodeID8: #[b {{paragraph.resNodeId | id8}}]
+        .col-xs-2
+            +result-toolbar
+
+mixin notebook-rename
+    .docs-header.notebook-header
+        h1.col-sm-6(ng-hide='notebook.edit')
+            label(style='max-width: calc(100% - 60px)') {{notebook.name}}
+            .btn-group(ng-if='!demo')
+                +btn-toolbar('fa-pencil', 'notebook.edit = true;notebook.editName = notebook.name', 'Rename notebook')
+                +btn-toolbar('fa-trash', 'removeNotebook(notebook)', 'Remove notebook')
+        h1.col-sm-6(ng-show='notebook.edit')
+            i.btn.fa.fa-floppy-o(ng-show='notebook.editName' ng-click='renameNotebook(notebook.editName)' bs-tooltip data-title='Save notebook name' data-trigger='hover')
+            .input-tip
+                input.form-control(ng-model='notebook.editName' required ignite-on-enter='renameNotebook(notebook.editName)' ignite-on-escape='notebook.edit = false;')
+        h1.pull-right
+            a.dropdown-toggle(style='margin-right: 20px' data-toggle='dropdown' bs-dropdown='scrollParagraphs' data-placement='bottom-right') Scroll to query
+                span.caret
+            button.btn.btn-default(style='margin-top: 2px' ng-click='addQuery()' ignite-on-click-focus=focusId)
+                i.fa.fa-fw.fa-plus
+                | Add query
+
+            button.btn.btn-default(style='margin-top: 2px' ng-click='addScan()' ignite-on-click-focus=focusId)
+                i.fa.fa-fw.fa-plus
+                | Add scan
+
+mixin notebook-error
+    h2 Failed to load notebook
+    label.col-sm-12 Notebook not accessible any more. Go back to configuration or open to another notebook.
+    button.h3.btn.btn-primary(ui-sref='base.configuration.tabs.advanced.clusters') Back to configuration
+
+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
+    .panel-top-align
+        label.tipLabel(bs-tooltip data-placement='bottom' data-title='Configure periodical execution of last successfully executed query') Refresh rate:
+            button.btn.btn-default.fa.fa-clock-o.tipLabel(ng-class='{"btn-info": paragraph.rate && paragraph.rate.installed}' bs-popover data-template-url='{{ $ctrl.paragraphRateTemplateUrl }}' data-placement='left' data-auto-close='1' data-trigger='click') {{rateAsString(paragraph)}}
+
+        label.tipLabel(bs-tooltip data-placement='bottom' data-title='Max number of rows to show in query result as one page') Page size:
+            button.btn.btn-default.select-toggle.tipLabel(ng-model='paragraph.pageSize' bs-select bs-options='item for item in pageSizes')
+
+        label.tipLabel(bs-tooltip data-placement='bottom' data-title='Limit query max results to specified number of pages') Max pages:
+            button.btn.btn-default.select-toggle.tipLabel(ng-model='paragraph.maxPages' bs-select bs-options='item.value as item.label for item in maxPages')
+
+        .panel-tip-container
+            .row(ng-if='nonCollocatedJoinsAvailable(paragraph)')
+                label.tipLabel(bs-tooltip data-placement='bottom' data-title='Non-collocated joins is a special mode that allow to join data across cluster without collocation.<br/>\
+                    Nested joins are not supported for now.<br/>\
+                    <b>NOTE</b>: In some cases it may consume more heap memory or may take a long time than collocated joins.' data-trigger='hover')
+                    input(type='checkbox' ng-model='paragraph.nonCollocatedJoins')
+                    span Allow non-collocated joins
+            .row(ng-if='enforceJoinOrderAvailable(paragraph)')
+                label.tipLabel(bs-tooltip data-placement='bottom' data-title='Enforce join order of tables in the query.<br/>\
+                    If <b>set</b>, then query optimizer will not reorder tables within join.<br/>\
+                    <b>NOTE:</b> It is not recommended to enable this property unless you have verified that\
+                    indexes are not selected in optimal order.' data-trigger='hover')
+                    input(type='checkbox' ng-model='paragraph.enforceJoinOrder')
+                    span Enforce join order
+            .row(ng-if='lazyQueryAvailable(paragraph)')
+                label.tipLabel(bs-tooltip data-placement='bottom' data-title='By default Ignite attempts to fetch the whole query result set to memory and send it to the client.<br/>\
+                    For small and medium result sets this provides optimal performance and minimize duration of internal database locks, thus increasing concurrency.<br/>\
+                    If result set is too big to fit in available memory this could lead to excessive GC pauses and even OutOfMemoryError.<br/>\
+                    Use this flag as a hint for Ignite to fetch result set lazily, thus minimizing memory consumption at the cost of moderate performance hit.' data-trigger='hover')
+                    input(type='checkbox' ng-model='paragraph.lazy')
+                    span Lazy result set
+
+mixin query-actions
+    button.btn.btn-primary(ng-disabled='!queryAvailable(paragraph)' ng-click='execute(paragraph)')
+        div
+            i.fa.fa-fw.fa-play(ng-hide='paragraph.executionInProgress(false)')
+            i.fa.fa-fw.fa-refresh.fa-spin(ng-show='paragraph.executionInProgress(false)')
+            span.tipLabelExecute Execute
+    button.btn.btn-primary(ng-disabled='!queryAvailable(paragraph)' ng-click='execute(paragraph, true)')
+        div
+            i.fa.fa-fw.fa-play(ng-hide='paragraph.executionInProgress(true)')
+            i.fa.fa-fw.fa-refresh.fa-spin(ng-show='paragraph.executionInProgress(true)')
+            span.tipLabelExecute Execute on selected node
+
+
+    a.btn.btn-default(ng-disabled='!queryAvailable(paragraph)' ng-click='explain(paragraph)' data-placement='bottom' bs-tooltip='' data-title='{{queryTooltip(paragraph, "explain query")}}') Explain
+
+mixin table-result-heading-query
+    .total.row
+        .col-xs-7
+            grid-column-selector(grid-api='paragraph.gridOptions.api')
+                .fa.fa-bars.icon
+            label Page: #[b {{paragraph.page}}]
+            label.margin-left-dflt Results so far: #[b {{paragraph.rows.length + paragraph.total}}]
+            label.margin-left-dflt Duration: #[b {{paragraph.duration | duration}}]
+            label.margin-left-dflt(ng-show='paragraph.localQueryMode') NodeID8: #[b {{paragraph.resNodeId | id8}}]
+        .col-xs-2
+            div(ng-if='paragraph.qryType === "query"')
+                +result-toolbar
+        .col-xs-3
+            .pull-right
+                .btn-group.panel-tip-container
+                    button.btn.btn-primary.btn--with-icon(
+                        ng-click='exportCsv(paragraph)'
+
+                        ng-disabled='paragraph.loading'
+
+                        bs-tooltip=''
+                        ng-attr-title='{{ queryTooltip(paragraph, "export query results") }}'
+
+                        data-trigger='hover'
+                        data-placement='bottom'
+                    )
+                        svg(ignite-icon='csv' ng-if='!paragraph.csvIsPreparing')
+                        i.fa.fa-fw.fa-refresh.fa-spin(ng-if='paragraph.csvIsPreparing')
+                        span Export
+
+                    -var options = [{ text: 'Export', click: 'exportCsv(paragraph)' }, { text: 'Export all', click: 'exportCsvAll(paragraph)' }, { divider: true }, { text: '<span title="Copy current result page to clipboard">Copy to clipboard</span>', click: 'exportCsvToClipBoard(paragraph)' }]
+                    button.btn.dropdown-toggle.btn-primary(
+                        ng-disabled='paragraph.loading'
+
+                        bs-dropdown=`${JSON.stringify(options)}`
+
+                        data-toggle='dropdown'
+                        data-container='body'
+                        data-placement='bottom-right'
+                        data-html='true'
+                    )
+                        span.caret
+
+
+
+mixin table-result-heading-scan
+    .total.row
+        .col-xs-7
+            grid-column-selector(grid-api='paragraph.gridOptions.api')
+                .fa.fa-bars.icon
+            label Page: #[b {{paragraph.page}}]
+            label.margin-left-dflt Results so far: #[b {{paragraph.rows.length + paragraph.total}}]
+            label.margin-left-dflt Duration: #[b {{paragraph.duration | duration}}]
+            label.margin-left-dflt(ng-show='paragraph.localQueryMode') NodeID8: #[b {{paragraph.resNodeId | id8}}]
+        .col-xs-2
+            div(ng-if='paragraph.qryType === "query"')
+                +result-toolbar
+        .col-xs-3
+            .pull-right
+                .btn-group.panel-tip-container
+                    // TODO: replace this logic for exporting under one component
+                    button.btn.btn-primary.btn--with-icon(
+                        ng-click='exportCsv(paragraph)'
+
+                        ng-disabled='paragraph.loading || paragraph.csvIsPreparing'
+
+                        bs-tooltip=''
+                        ng-attr-title='{{ scanTooltip(paragraph) }}'
+
+                        data-trigger='hover'
+                        data-placement='bottom'
+                    )
+                        svg(ignite-icon='csv' ng-if='!paragraph.csvIsPreparing')
+                        i.fa.fa-fw.fa-refresh.fa-spin(ng-if='paragraph.csvIsPreparing')
+                        span Export
+
+                    -var options = [{ text: "Export", click: 'exportCsv(paragraph)' }, { text: 'Export all', click: 'exportCsvAll(paragraph)' }, { divider: true }, { text: '<span title="Copy current result page to clipboard">Copy to clipboard</span>', click: 'exportCsvToClipBoard(paragraph)' }]
+                    button.btn.dropdown-toggle.btn-primary(
+                        ng-disabled='paragraph.loading || paragraph.csvIsPreparing'
+
+                        bs-dropdown=`${JSON.stringify(options)}`
+
+                        data-toggle='dropdown'
+                        data-container='body'
+                        data-placement='bottom-right'
+                        data-html='true'
+                    )
+                        span.caret
+
+mixin table-result-body
+    .grid(ui-grid='paragraph.gridOptions' ui-grid-resize-columns ui-grid-exporter)
+
+mixin chart-result
+    div(ng-hide='paragraph.scanExplain()')
+        +chart-settings
+        .empty(ng-show='paragraph.chartColumns.length > 0 && !paragraph.chartColumnsConfigured()') Cannot display chart. Please configure axis using #[b Chart settings]
+        .empty(ng-show='paragraph.chartColumns.length == 0') Cannot display chart. Result set must contain Java build-in type columns. Please change query and execute it again.
+        div(ng-show='paragraph.chartColumnsConfigured()')
+            div(ng-show='paragraph.timeLineSupported() || !paragraph.chartTimeLineEnabled()')
+                div(ng-repeat='chart in paragraph.charts')
+                    nvd3(options='chart.options' data='chart.data' api='chart.api')
+            .empty(ng-show='!paragraph.timeLineSupported() && paragraph.chartTimeLineEnabled()') Pie chart does not support 'TIME_LINE' column for X-axis. Please use another column for X-axis or switch to another chart.
+    .empty(ng-show='paragraph.scanExplain()')
+        .row
+            .col-xs-4.col-xs-offset-4
+                +result-toolbar
+        label.margin-top-dflt Charts do not support #[b Explain] and #[b Scan] query
+
+mixin paragraph-scan
+    .panel-heading(bs-collapse-toggle)
+        .row
+            +paragraph-rename
+    .panel-collapse(role='tabpanel' bs-collapse-target)
+        .col-sm-12.sql-controls
+            .col-sm-3
+                +dropdown-required('Cache:', 'paragraph.cacheName', '"cache"', 'true', 'false', 'Choose cache', 'caches')
+            .col-sm-3
+                +text-enabled('Filter:', 'paragraph.filter', '"filter"', true, false, 'Enter filter')
+                    label.btn.btn-default.ignite-form-field__btn(ng-click='paragraph.caseSensitive = !paragraph.caseSensitive')
+                        input(type='checkbox' ng-model='paragraph.caseSensitive')
+                        span(bs-tooltip data-title='Select this checkbox for case sensitive search') Cs
+            label.tipLabel(bs-tooltip data-placement='bottom' data-title='Max number of rows to show in query result as one page') Page size:
+                button.btn.btn-default.select-toggle.tipLabel(ng-model='paragraph.pageSize' bs-select bs-options='item for item in pageSizes')
+
+        .col-sm-12.sql-controls
+            button.btn.btn-primary(ng-disabled='!scanAvailable(paragraph)' ng-click='scan(paragraph)')
+                div
+                    i.fa.fa-fw.fa-play(ng-hide='paragraph.checkScanInProgress(false)')
+                    i.fa.fa-fw.fa-refresh.fa-spin(ng-show='paragraph.checkScanInProgress(false)')
+                    span.tipLabelExecute Scan
+
+            button.btn.btn-primary(ng-disabled='!scanAvailable(paragraph)' ng-click='scan(paragraph, true)')
+                    i.fa.fa-fw.fa-play(ng-hide='paragraph.checkScanInProgress(true)')
+                    i.fa.fa-fw.fa-refresh.fa-spin(ng-show='paragraph.checkScanInProgress(true)')
+                    span.tipLabelExecute Scan on selected node
+
+        .col-sm-12.sql-result(ng-if='paragraph.queryExecuted()' ng-switch='paragraph.resultType()')
+            .error(ng-switch-when='error') Error: {{paragraph.error.message}}
+            .empty(ng-switch-when='empty') Result set is empty. Duration: #[b {{paragraph.duration | duration}}]
+            .table(ng-switch-when='table')
+                +table-result-heading-scan
+                +table-result-body
+            .footer.clearfix()
+                .pull-left
+                    | Showing results for scan of #[b {{ paragraph.queryArgs.cacheName | defaultName }}]
+                    span(ng-if='paragraph.queryArgs.filter') &nbsp; with filter: #[b {{ paragraph.queryArgs.filter }}]
+                    span(ng-if='paragraph.queryArgs.localNid') &nbsp; on node: #[b {{ paragraph.queryArgs.localNid | limitTo:8 }}]
+
+                -var nextVisibleCondition = 'paragraph.resultType() != "error" && paragraph.queryId && paragraph.nonRefresh() && (paragraph.table() || paragraph.chart() && !paragraph.scanExplain())'
+
+                .pull-right(ng-show=`${nextVisibleCondition}` ng-class='{disabled: paragraph.loading}' ng-click='!paragraph.loading && nextPage(paragraph)')
+                    i.fa.fa-chevron-circle-right
+                    a Next
+
+mixin paragraph-query
+    .row.panel-heading(bs-collapse-toggle)
+        +paragraph-rename
+    .panel-collapse(role='tabpanel' bs-collapse-target)
+        .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"],' +
+                'advanced: {enableSnippets: false, enableBasicAutocompletion: true, enableLiveAutocompletion: true}}'
+                ng-model='paragraph.query')
+            .col-xs-4.col-sm-3
+                div(ng-show='caches.length > 0' style='padding: 5px 10px' st-table='displayedCaches' st-safe-src='caches')
+                    lable.labelField.labelFormField Caches:
+                    i.fa.fa-database.tipField(title='Click to show cache types metadata dialog' bs-popover data-template-url='{{ $ctrl.cacheMetadataTemplateUrl }}' data-placement='bottom' data-trigger='click' data-container='#{{ paragraph.id }}')
+                    .input-tip
+                        input.form-control(type='text' st-search='label' placeholder='Filter caches...')
+                    table.links
+                        tbody.scrollable-y(style='max-height: 15em; display: block;')
+                            tr(ng-repeat='cache in displayedCaches track by cache.name')
+                                td(style='width: 100%')
+                                    input.labelField(id='cache_{{ [paragraph.id, $index].join("_") }}' type='radio' value='{{cache.name}}' ng-model='paragraph.cacheName')
+                                    label(for='cache_{{ [paragraph.id, $index].join("_") }} ' ng-bind-html='cache.label')
+                    .settings-row
+                        .row(ng-if='ddlAvailable(paragraph)')
+                            label.tipLabel.use-cache(bs-tooltip data-placement='bottom'
+                                data-title=
+                                    'Use selected cache as default schema name.<br/>\
+                                    This will allow to execute query on specified cache without specify schema name.<br/>\
+                                    <b>NOTE:</b> In future version of Ignite this feature will be removed.'
+                                data-trigger='hover')
+                                input(type='checkbox' ng-model='paragraph.useAsDefaultSchema')
+                                span Use selected cache as default schema name
+                .empty-caches(ng-show='displayedCaches.length == 0 && caches.length != 0')
+                    label Wrong caches filter
+                .empty-caches(ng-show='caches.length == 0')
+                    label No caches
+        .col-sm-12.sql-controls
+            +query-actions
+
+            .pull-right
+                +query-settings
+        .col-sm-12.sql-result(ng-if='paragraph.queryExecuted()' ng-switch='paragraph.resultType()')
+            .error(ng-switch-when='error')
+                label Error: {{paragraph.error.message}}
+                br
+                a(ng-show='paragraph.resultType() === "error"' ng-click='showStackTrace(paragraph)') Show more
+            .empty(ng-switch-when='empty') Result set is empty. Duration: #[b {{paragraph.duration | duration}}]
+            .table(ng-switch-when='table')
+                +table-result-heading-query
+                +table-result-body
+            .chart(ng-switch-when='chart')
+                +chart-result
+            .footer.clearfix(ng-show='paragraph.resultType() !== "error"')
+                a.pull-left(ng-click='showResultQuery(paragraph)') Show query
+
+                -var nextVisibleCondition = 'paragraph.resultType() !== "error" && paragraph.queryId && paragraph.nonRefresh() && (paragraph.table() || paragraph.chart() && !paragraph.scanExplain())'
+
+                .pull-right(ng-show=`${nextVisibleCondition}` ng-class='{disabled: paragraph.loading}' ng-click='!paragraph.loading && nextPage(paragraph)')
+                    i.fa.fa-chevron-circle-right
+                    a Next
+
+.row
+    .docs-content
+        header
+            h1 Queries
+            cluster-selector
+
+        .row(ng-if='notebook' bs-affix style='margin-bottom: 20px;')
+            +notebook-rename
+
+        ignite-information(data-title='With query notebook you can' style='margin-bottom: 30px')
+            ul
+                li Create any number of queries
+                li Execute and explain SQL queries
+                li Execute scan queries
+                li View data in tabular form and as charts
+
+        div(ng-if='notebookLoadFailed' style='text-align: center')
+            +notebook-error
+
+        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-paragraph(ng-repeat='paragraph in notebook.paragraphs' id='{{paragraph.id}}' ng-form='form_{{paragraph.id}}')
+                        .panel.panel-default(ng-if='paragraph.qryType === "scan"')
+                            +paragraph-scan
+                        .panel.panel-default(ng-if='paragraph.qryType === "query"')
+                            +paragraph-query

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/modules/agent/AgentManager.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/agent/AgentManager.service.js b/modules/web-console/frontend/app/modules/agent/AgentManager.service.js
index 752b4f0..7668132 100644
--- a/modules/web-console/frontend/app/modules/agent/AgentManager.service.js
+++ b/modules/web-console/frontend/app/modules/agent/AgentManager.service.js
@@ -38,7 +38,18 @@ class ConnectionState {
         this.state = State.DISCONNECTED;
     }
 
+    updateCluster(cluster) {
+        this.cluster = cluster;
+        this.cluster.connected = !!_.find(this.clusters, {id: this.cluster.id});
+
+        return cluster;
+    }
+
     update(demo, count, clusters) {
+        _.forEach(clusters, (cluster) => {
+            cluster.name = cluster.id;
+        });
+
         this.clusters = clusters;
 
         if (_.isNil(this.cluster))
@@ -142,6 +153,7 @@ export default class IgniteAgentManager {
         };
 
         self.socket.on('connect_error', onDisconnect);
+
         self.socket.on('disconnect', onDisconnect);
 
         self.socket.on('agents:stat', ({clusters, count}) => {
@@ -152,6 +164,8 @@ export default class IgniteAgentManager {
             self.connectionSbj.next(conn);
         });
 
+        self.socket.on('cluster:changed', (cluster) => this.updateCluster(cluster));
+
         self.socket.on('user:notifications', (notification) => this.UserNotifications.notification = notification);
     }
 
@@ -163,6 +177,31 @@ export default class IgniteAgentManager {
         }
     }
 
+    updateCluster(newCluster) {
+        const state = this.connectionSbj.getValue();
+
+        const oldCluster = _.find(state.clusters, (cluster) => cluster.id === newCluster.id);
+
+        if (!_.isNil(oldCluster)) {
+            oldCluster.nids = newCluster.nids;
+            oldCluster.addresses = newCluster.addresses;
+            oldCluster.clusterVersion = newCluster.clusterVersion;
+            oldCluster.active = newCluster.active;
+
+            this.connectionSbj.next(state);
+        }
+    }
+
+    switchCluster(cluster) {
+        const state = this.connectionSbj.getValue();
+
+        state.updateCluster(cluster);
+
+        this.connectionSbj.next(state);
+
+        this.saveToStorage(cluster);
+    }
+
     /**
      * @param states
      * @returns {Promise}
@@ -212,6 +251,8 @@ export default class IgniteAgentManager {
 
         self.connectionSbj.next(conn);
 
+        this.modalSubscription && this.modalSubscription.unsubscribe();
+
         self.modalSubscription = this.connectionSbj.subscribe({
             next: ({state}) => {
                 switch (state) {
@@ -252,6 +293,8 @@ export default class IgniteAgentManager {
 
         self.connectionSbj.next(conn);
 
+        this.modalSubscription && this.modalSubscription.unsubscribe();
+
         self.modalSubscription = this.connectionSbj.subscribe({
             next: ({state}) => {
                 switch (state) {
@@ -633,7 +676,6 @@ export default class IgniteAgentManager {
     }
 
     /**
-     /**
      * @param {String} nid Node id.
      * @param {String} cacheName Cache name.
      * @param {String} filter Filter text.
@@ -664,4 +706,17 @@ export default class IgniteAgentManager {
         return this.queryScan(nid, cacheName, filter, regEx, caseSensitive, near, local, pageSz)
             .then(fetchResult);
     }
+
+    /**
+     * Change cluster active state.
+     *
+     * @returns {Promise}
+     */
+    toggleClusterState() {
+        const state = this.connectionSbj.getValue();
+        const active = !state.cluster.active;
+
+        return this.visorTask('toggleClusterState', null, active)
+            .then(() => state.updateCluster(Object.assign(state.cluster, { active })));
+    }
 }

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/modules/sql/Notebook.data.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/sql/Notebook.data.js b/modules/web-console/frontend/app/modules/sql/Notebook.data.js
deleted file mode 100644
index 3f98bed..0000000
--- a/modules/web-console/frontend/app/modules/sql/Notebook.data.js
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-const DEMO_NOTEBOOK = {
-    name: 'SQL demo',
-    paragraphs: [
-        {
-            name: 'Query with refresh rate',
-            cacheName: 'CarCache',
-            pageSize: 100,
-            limit: 0,
-            query: [
-                'SELECT count(*)',
-                'FROM "CarCache".Car'
-            ].join('\n'),
-            result: 'bar',
-            timeLineSpan: '1',
-            rate: {
-                value: 3,
-                unit: 1000,
-                installed: true
-            }
-        },
-        {
-            name: 'Simple query',
-            cacheName: 'CarCache',
-            pageSize: 100,
-            limit: 0,
-            query: 'SELECT * FROM "CarCache".Car',
-            result: 'table',
-            timeLineSpan: '1',
-            rate: {
-                value: 30,
-                unit: 1000,
-                installed: false
-            }
-        },
-        {
-            name: 'Query with aggregates',
-            cacheName: 'ParkingCache',
-            pageSize: 100,
-            limit: 0,
-            query: [
-                'SELECT p.name, count(*) AS cnt',
-                'FROM "ParkingCache".Parking p',
-                'INNER JOIN "CarCache".Car c',
-                '  ON (p.id) = (c.parkingId)',
-                'GROUP BY P.NAME'
-            ].join('\n'),
-            result: 'table',
-            timeLineSpan: '1',
-            rate: {
-                value: 30,
-                unit: 1000,
-                installed: false
-            }
-        }
-    ],
-    expandedParagraphs: [0, 1, 2]
-};
-
-export default class NotebookData {
-    static $inject = ['$rootScope', '$http', '$q'];
-
-    constructor($root, $http, $q) {
-        this.demo = $root.IgniteDemoMode;
-
-        this.initLatch = null;
-        this.notebooks = null;
-
-        this.$http = $http;
-        this.$q = $q;
-    }
-
-    load() {
-        if (this.demo) {
-            if (this.initLatch)
-                return this.initLatch;
-
-            return this.initLatch = this.$q.when(this.notebooks = [DEMO_NOTEBOOK]);
-        }
-
-        return this.initLatch = this.$http.get('/api/v1/notebooks')
-            .then(({data}) => this.notebooks = data)
-            .catch(({data}) => Promise.reject(data));
-    }
-
-    read() {
-        if (this.initLatch)
-            return this.initLatch;
-
-        return this.load();
-    }
-
-    find(_id) {
-        return this.read()
-            .then(() => {
-                const notebook = this.demo ? this.notebooks[0] : _.find(this.notebooks, {_id});
-
-                if (_.isNil(notebook))
-                    return this.$q.reject('Failed to load notebook.');
-
-                return notebook;
-            });
-    }
-
-    findIndex(notebook) {
-        return this.read()
-            .then(() => _.findIndex(this.notebooks, {_id: notebook._id}));
-    }
-
-    save(notebook) {
-        if (this.demo)
-            return this.$q.when(DEMO_NOTEBOOK);
-
-        return this.$http.post('/api/v1/notebooks/save', notebook)
-            .then(({data}) => {
-                const idx = _.findIndex(this.notebooks, {_id: data._id});
-
-                if (idx >= 0)
-                    this.notebooks[idx] = data;
-                else
-                    this.notebooks.push(data);
-
-                return data;
-            })
-            .catch(({data}) => Promise.reject(data));
-    }
-
-    remove(notebook) {
-        if (this.demo)
-            return this.$q.reject(`Removing "${notebook.name}" notebook is not supported.`);
-
-        const key = {_id: notebook._id};
-
-        return this.$http.post('/api/v1/notebooks/remove', key)
-            .then(() => {
-                const idx = _.findIndex(this.notebooks, key);
-
-                if (idx >= 0) {
-                    this.notebooks.splice(idx, 1);
-
-                    if (idx < this.notebooks.length)
-                        return this.notebooks[idx];
-                }
-
-                if (this.notebooks.length > 0)
-                    return this.notebooks[this.notebooks.length - 1];
-
-                return null;
-            })
-            .catch(({data}) => Promise.reject(data));
-    }
-}

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/modules/sql/Notebook.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/sql/Notebook.service.js b/modules/web-console/frontend/app/modules/sql/Notebook.service.js
deleted file mode 100644
index b0bb64f..0000000
--- a/modules/web-console/frontend/app/modules/sql/Notebook.service.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export default class Notebook {
-    static $inject = ['$state', 'IgniteConfirm', 'IgniteMessages', 'IgniteNotebookData'];
-
-    /**
-     * @param $state
-     * @param confirmModal
-     * @param Messages
-     * @param {NotebookData} NotebookData
-     */
-    constructor($state, confirmModal, Messages, NotebookData) {
-        this.$state = $state;
-        this.confirmModal = confirmModal;
-        this.Messages = Messages;
-        this.NotebookData = NotebookData;
-    }
-
-    read() {
-        return this.NotebookData.read();
-    }
-
-    create(name) {
-        return this.NotebookData.save({name});
-    }
-
-    save(notebook) {
-        return this.NotebookData.save(notebook);
-    }
-
-    find(_id) {
-        return this.NotebookData.find(_id);
-    }
-
-    _openNotebook(idx) {
-        return this.NotebookData.read()
-            .then((notebooks) => {
-                const nextNotebook = notebooks.length > idx ? notebooks[idx] : _.last(notebooks);
-
-                if (nextNotebook)
-                    this.$state.go('base.sql.notebook', {noteId: nextNotebook._id});
-                else
-                    this.$state.go('base.configuration.tabs.advanced.clusters');
-            });
-    }
-
-    remove(notebook) {
-        return this.confirmModal.confirm(`Are you sure you want to remove notebook: "${notebook.name}"?`)
-            .then(() => this.NotebookData.findIndex(notebook))
-            .then((idx) => {
-                this.NotebookData.remove(notebook)
-                    .then(() => {
-                        if (this.$state.includes('base.sql.notebook') && this.$state.params.noteId === notebook._id)
-                            return this._openNotebook(idx);
-                    })
-                    .catch(this.Messages.showError);
-            });
-    }
-}

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/modules/sql/notebook.controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/sql/notebook.controller.js b/modules/web-console/frontend/app/modules/sql/notebook.controller.js
deleted file mode 100644
index 68d318a..0000000
--- a/modules/web-console/frontend/app/modules/sql/notebook.controller.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import templateUrl from 'views/sql/notebook-new.tpl.pug';
-
-// Controller that load notebooks in navigation bar .
-export default ['$scope', '$modal', '$state', 'IgniteMessages', 'IgniteNotebook',
-    (scope, $modal, $state, Messages, Notebook) => {
-        // Pre-fetch modal dialogs.
-        const nameModal = $modal({scope, templateUrl, show: false});
-
-        scope.create = (name) => {
-            return Notebook.create(name)
-                .then((notebook) => {
-                    nameModal.hide();
-
-                    $state.go('base.sql.notebook', {noteId: notebook._id});
-                })
-                .catch(Messages.showError);
-        };
-
-        scope.createNotebook = () => nameModal.$promise.then(nameModal.show);
-
-        Notebook.read()
-            .then((notebooks) => {
-                scope.$watchCollection(() => notebooks, (changed) => {
-                    if (_.isEmpty(changed))
-                        return scope.notebooks = [];
-
-                    scope.notebooks = [
-                        {text: 'Create new notebook', click: scope.createNotebook},
-                        {divider: true}
-                    ];
-
-                    _.forEach(changed, (notebook) => scope.notebooks.push({
-                        data: notebook,
-                        action: {
-                            icon: 'fa-trash',
-                            click: (item) => Notebook.remove(item)
-                        },
-                        text: notebook.name,
-                        sref: `base.sql.notebook({noteId:"${notebook._id}"})`
-                    }));
-                });
-            })
-            .catch(Messages.showError);
-    }
-];


[5/5] ignite git commit: IGNITE-6390 Web Console: Added component for cluster selection.

Posted by ak...@apache.org.
IGNITE-6390 Web Console: Added component for cluster selection.


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

Branch: refs/heads/master
Commit: 1367bc98eb08233f9e47ba45335f9dda1fbb7bbd
Parents: cbd69d6
Author: Dmitriy Shabalin <ds...@gridgain.com>
Authored: Wed Dec 6 10:36:42 2017 +0700
Committer: Alexey Kuznetsov <ak...@apache.org>
Committed: Wed Dec 6 10:36:42 2017 +0700

----------------------------------------------------------------------
 .../internal/visor/util/VisorTaskUtils.java     |  139 ++
 .../commands/tasks/VisorTasksCommand.scala      |    1 +
 .../scala/org/apache/ignite/visor/visor.scala   |   49 -
 modules/web-console/backend/app/agentSocket.js  |    2 +-
 .../web-console/backend/app/agentsHandler.js    |   51 +-
 .../web-console/backend/app/browsersHandler.js  |    7 +
 modules/web-console/backend/app/mongo.js        |    1 +
 modules/web-console/backend/package.json        |    3 +-
 modules/web-console/frontend/app/app.js         |    8 +-
 .../app/components/bs-select-menu/style.scss    |    4 +-
 .../cluster-select/cluster-select.controller.js |   64 -
 .../cluster-select/cluster-select.pug           |   47 -
 .../cluster-select/cluster-select.scss          |   30 -
 .../app/components/cluster-select/index.js      |   29 -
 .../components/cluster-selector/component.js    |   25 +
 .../components/cluster-selector/controller.js   |   62 +
 .../app/components/cluster-selector/index.js    |   23 +
 .../app/components/cluster-selector/style.scss  |   66 +
 .../components/cluster-selector/template.pug    |   75 +
 .../app/components/list-editable/controller.js  |    2 +-
 .../components/page-queries/Notebook.data.js    |  168 ++
 .../components/page-queries/Notebook.service.js |   74 +
 .../app/components/page-queries/controller.js   | 1938 ++++++++++++++++++
 .../app/components/page-queries/index.js        |   62 +
 .../page-queries/notebook.controller.js         |   62 +
 .../app/components/page-queries/style.scss      |   36 +
 .../components/page-queries/template.tpl.pug    |  385 ++++
 .../app/modules/agent/AgentManager.service.js   |   57 +-
 .../frontend/app/modules/sql/Notebook.data.js   |  168 --
 .../app/modules/sql/Notebook.service.js         |   74 -
 .../app/modules/sql/notebook.controller.js      |   62 -
 .../frontend/app/modules/sql/sql.controller.js  | 1887 -----------------
 .../frontend/app/modules/sql/sql.module.js      |   61 -
 .../frontend/app/primitives/switcher/index.pug  |    2 +-
 .../frontend/app/primitives/switcher/index.scss |   69 +-
 .../frontend/views/includes/header-right.pug    |    2 -
 .../web-console/frontend/views/sql/sql.tpl.pug  |  381 ----
 .../console/agent/handlers/ClusterListener.java |  178 +-
 .../ignite/console/agent/rest/RestExecutor.java |   63 +-
 39 files changed, 3477 insertions(+), 2940 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/core/src/main/java/org/apache/ignite/internal/visor/util/VisorTaskUtils.java
----------------------------------------------------------------------
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/util/VisorTaskUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/util/VisorTaskUtils.java
index ace451c..fda801c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/visor/util/VisorTaskUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/util/VisorTaskUtils.java
@@ -23,8 +23,10 @@ import java.io.FileFilter;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.RandomAccessFile;
+import java.math.BigDecimal;
 import java.net.InetAddress;
 import java.net.URL;
+import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.charset.CharacterCodingException;
@@ -69,6 +71,7 @@ import org.apache.ignite.internal.visor.log.VisorLogFile;
 import org.apache.ignite.lang.IgniteClosure;
 import org.apache.ignite.lang.IgnitePredicate;
 import org.apache.ignite.spi.eventstorage.NoopEventStorageSpi;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import static java.lang.System.getProperty;
@@ -1113,4 +1116,140 @@ public class VisorTaskUtils {
     public static boolean joinTimedOut(String msg) {
         return msg != null && msg.startsWith("Join process timed out.");
     }
+
+    /**
+     * Special wrapper over address that can be sorted in following order:
+     *     IPv4, private IPv4, IPv4 local host, IPv6.
+     *     Lower addresses first.
+     */
+    private static class SortableAddress implements Comparable<SortableAddress> {
+        /** */
+        private int type;
+
+        /** */
+        private BigDecimal bits;
+
+        /** */
+        private String addr;
+
+        /**
+         * Constructor.
+         *
+         * @param addr Address as string.
+         */
+        private SortableAddress(String addr) {
+            this.addr = addr;
+
+            if (addr.indexOf(':') > 0)
+                type = 4; // IPv6
+            else {
+                try {
+                    InetAddress inetAddr = InetAddress.getByName(addr);
+
+                    if (inetAddr.isLoopbackAddress())
+                        type = 3;  // localhost
+                    else if (inetAddr.isSiteLocalAddress())
+                        type = 2;  // private IPv4
+                    else
+                        type = 1; // other IPv4
+                }
+                catch (UnknownHostException ignored) {
+                    type = 5;
+                }
+            }
+
+            bits = BigDecimal.valueOf(0L);
+
+            try {
+                String[] octets = addr.contains(".") ? addr.split(".") : addr.split(":");
+
+                int len = octets.length;
+
+                for (int i = 0; i < len; i++) {
+                    long oct = F.isEmpty(octets[i]) ? 0 : Long.valueOf( octets[i]);
+                    long pow = Double.valueOf(Math.pow(256, octets.length - 1 - i)).longValue();
+
+                    bits = bits.add(BigDecimal.valueOf(oct * pow));
+                }
+            }
+            catch (Exception ignore) {
+                // No-op.
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override public int compareTo(@NotNull SortableAddress o) {
+            return (type == o.type ? bits.compareTo(o.bits) : Integer.compare(type, o.type));
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean equals(Object o) {
+            if (this == o)
+                return true;
+
+            if (o == null || getClass() != o.getClass())
+                return false;
+
+            SortableAddress other = (SortableAddress)o;
+
+            return addr != null ? addr.equals(other.addr) : other.addr == null;
+        }
+
+        /** {@inheritDoc} */
+        @Override public int hashCode() {
+            return addr != null ? addr.hashCode() : 0;
+        }
+
+        /**
+         * @return Address.
+         */
+        public String address() {
+            return addr;
+        }
+    }
+
+    /**
+     * Sort addresses: IPv4 & real addresses first.
+     *
+     * @param addrs Addresses to sort.
+     * @return Sorted list.
+     */
+    public static Collection<String> sortAddresses(Collection<String> addrs) {
+        if (F.isEmpty(addrs))
+            return Collections.emptyList();
+
+        int sz = addrs.size();
+
+        List<SortableAddress> sorted = new ArrayList<>(sz);
+
+        for (String addr : addrs)
+            sorted.add(new SortableAddress(addr));
+
+        Collections.sort(sorted);
+
+        Collection<String> res = new ArrayList<>(sz);
+
+        for (SortableAddress sa : sorted)
+            res.add(sa.address());
+
+        return res;
+    }
+
+    /**
+     * Split addresses.
+     *
+     * @param s String with comma separted addresses.
+     * @return Collection of addresses.
+     */
+    public static Collection<String> splitAddresses(String s) {
+        if (F.isEmpty(s))
+            return Collections.emptyList();
+
+        String[] addrs = s.split(",");
+
+        for (int i = 0; i < addrs.length; i++)
+            addrs[i] = addrs[i].trim();
+
+        return Arrays.asList(addrs);
+    }
 }

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/visor-console/src/main/scala/org/apache/ignite/visor/commands/tasks/VisorTasksCommand.scala
----------------------------------------------------------------------
diff --git a/modules/visor-console/src/main/scala/org/apache/ignite/visor/commands/tasks/VisorTasksCommand.scala b/modules/visor-console/src/main/scala/org/apache/ignite/visor/commands/tasks/VisorTasksCommand.scala
index 4d9b795..0d6753e 100644
--- a/modules/visor-console/src/main/scala/org/apache/ignite/visor/commands/tasks/VisorTasksCommand.scala
+++ b/modules/visor-console/src/main/scala/org/apache/ignite/visor/commands/tasks/VisorTasksCommand.scala
@@ -32,6 +32,7 @@ import java.util.UUID
 import org.apache.ignite.internal.visor.event.{VisorGridEvent, VisorGridJobEvent, VisorGridTaskEvent}
 import org.apache.ignite.internal.visor.node.VisorNodeEventsCollectorTask
 import org.apache.ignite.internal.visor.node.VisorNodeEventsCollectorTaskArg
+import org.apache.ignite.internal.visor.util.VisorTaskUtils._
 
 import scala.collection.JavaConversions._
 import scala.language.implicitConversions

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/visor-console/src/main/scala/org/apache/ignite/visor/visor.scala
----------------------------------------------------------------------
diff --git a/modules/visor-console/src/main/scala/org/apache/ignite/visor/visor.scala b/modules/visor-console/src/main/scala/org/apache/ignite/visor/visor.scala
index ffc7a00..1a46316 100644
--- a/modules/visor-console/src/main/scala/org/apache/ignite/visor/visor.scala
+++ b/modules/visor-console/src/main/scala/org/apache/ignite/visor/visor.scala
@@ -2694,53 +2694,4 @@ object visor extends VisorTag {
         else
             Long.MaxValue
     }
-
-    /**
-     * Sort addresses to properly display in Visor.
-     *
-     * @param addrs Addresses to sort.
-     * @return Sorted list.
-     */
-    def sortAddresses(addrs: Iterable[String]) = {
-        def ipToLong(ip: String) = {
-            try {
-                val octets = if (ip.contains(".")) ip.split('.') else ip.split(':')
-
-                var dec = BigDecimal.valueOf(0L)
-
-                for (i <- octets.indices) dec += octets(i).toLong * math.pow(256, octets.length - 1 - i).toLong
-
-                dec
-            }
-            catch {
-                case _: Exception => BigDecimal.valueOf(0L)
-            }
-        }
-
-        /**
-         * Sort addresses to properly display in Visor.
-         *
-         * @param addr Address to detect type for.
-         * @return IP class type for sorting in order: public addresses IPv4 + private IPv4 + localhost + IPv6.
-         */
-        def addrType(addr: String) = {
-            if (addr.contains(':'))
-                4 // IPv6
-            else {
-                try {
-                    InetAddress.getByName(addr) match {
-                        case ip if ip.isLoopbackAddress => 3 // localhost
-                        case ip if ip.isSiteLocalAddress => 2 // private IPv4
-                        case _ => 1 // other IPv4
-                    }
-                }
-                catch {
-                    case ignore: UnknownHostException => 5
-                }
-            }
-        }
-
-        addrs.map(addr => (addrType(addr), ipToLong(addr), addr)).toSeq.
-            sortWith((l, r) => if (l._1 == r._1) l._2.compare(r._2) < 0 else l._1 < r._1).map(_._3)
-    }
 }

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/backend/app/agentSocket.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/app/agentSocket.js b/modules/web-console/backend/app/agentSocket.js
index 75dcd53..6e4518a 100644
--- a/modules/web-console/backend/app/agentSocket.js
+++ b/modules/web-console/backend/app/agentSocket.js
@@ -88,7 +88,7 @@ module.exports.factory = function(_) {
     class AgentSocket {
         /**
          * @param {Socket} socket Socket for interaction.
-         * @param {String} tokens Active tokens.
+         * @param {Array.<String>} tokens Agent tokens.
          * @param {String} demoEnabled Demo enabled.
          */
         constructor(socket, tokens, demoEnabled) {

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/backend/app/agentsHandler.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/app/agentsHandler.js b/modules/web-console/backend/app/agentsHandler.js
index 112793a..844ce1e 100644
--- a/modules/web-console/backend/app/agentsHandler.js
+++ b/modules/web-console/backend/app/agentsHandler.js
@@ -17,6 +17,8 @@
 
 'use strict';
 
+const uuid = require('uuid/v4');
+
 // Fire me up!
 
 /**
@@ -82,19 +84,14 @@ module.exports.factory = function(_, fs, path, JSZip, socketio, settings, mongo,
 
     class Cluster {
         constructor(top) {
-            let d = new Date().getTime();
-
-            this.id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
-                const r = (d + Math.random() * 16) % 16 | 0;
-
-                d = Math.floor(d / 16);
-
-                return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
-            });
+            const clusterName = top.clusterName;
 
+            this.id = _.isEmpty(clusterName) ? `Cluster ${uuid().substring(0, 8).toUpperCase()}` : clusterName;
             this.nids = top.nids;
-
+            this.addresses = top.addresses;
+            this.clients = top.clients;
             this.clusterVersion = top.clusterVersion;
+            this.active = top.active;
         }
 
         isSameCluster(top) {
@@ -103,8 +100,18 @@ module.exports.factory = function(_, fs, path, JSZip, socketio, settings, mongo,
 
         update(top) {
             this.clusterVersion = top.clusterVersion;
-
             this.nids = top.nids;
+            this.addresses = top.addresses;
+            this.clients = top.clients;
+            this.clusterVersion = top.clusterVersion;
+            this.active = top.active;
+        }
+
+        same(top) {
+            return _.difference(this.nids, top.nids).length === 0 &&
+                _.isEqual(this.addresses, top.addresses) &&
+                this.clusterVersion === top.clusterVersion &&
+                this.active === top.active;
         }
     }
 
@@ -192,10 +199,13 @@ module.exports.factory = function(_, fs, path, JSZip, socketio, settings, mongo,
         }
 
         getOrCreateCluster(top) {
-            const cluster = _.find(this.clusters, (c) => c.isSameCluster(top));
+            let cluster = _.find(this.clusters, (c) => c.isSameCluster(top));
+
+            if (_.isNil(cluster)) {
+                cluster = new Cluster(top);
 
-            if (_.isNil(cluster))
-                this.clusters.push(new Cluster(top));
+                this.clusters.push(cluster);
+            }
 
             return cluster;
         }
@@ -230,8 +240,17 @@ module.exports.factory = function(_, fs, path, JSZip, socketio, settings, mongo,
                         this._browsersHnd.agentStats(token);
                     });
                 }
-                else
-                    cluster.update(top);
+                else {
+                    const changed = !cluster.same(top);
+
+                    if (changed) {
+                        cluster.update(top);
+
+                        _.forEach(tokens, (token) => {
+                            this._browsersHnd.clusterChanged(token, cluster);
+                        });
+                    }
+                }
             });
 
             sock.on('cluster:collector', (top) => {

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/backend/app/browsersHandler.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/app/browsersHandler.js b/modules/web-console/backend/app/browsersHandler.js
index 8b1385d..7ae247b 100644
--- a/modules/web-console/backend/app/browsersHandler.js
+++ b/modules/web-console/backend/app/browsersHandler.js
@@ -124,6 +124,12 @@ module.exports = {
                     .then((stat) => _.forEach(socks, (sock) => sock.emit('agents:stat', stat)));
             }
 
+            clusterChanged(token, cluster) {
+                const socks = this._browserSockets.get(token);
+
+                _.forEach(socks, (sock) => sock.emit('cluster:changed', cluster));
+            }
+
             emitNotification(sock) {
                 sock.emit('user:notifications', this.notification);
             }
@@ -224,6 +230,7 @@ module.exports = {
                 this.registerVisorTask('queryClose', internalVisor('query.VisorQueryCleanupTask'), 'java.util.Map', 'java.util.UUID', 'java.util.Set');
                 this.registerVisorTask('queryCloseX2', internalVisor('query.VisorQueryCleanupTask'), internalVisor('query.VisorQueryCleanupTaskArg'));
 
+                this.registerVisorTask('toggleClusterState', internalVisor('misc.VisorChangeGridActiveStateTask'), internalVisor('misc.VisorChangeGridActiveStateTaskArg'));
 
                 // Return command result from grid to browser.
                 sock.on('node:visor', (clusterId, taskId, nids, ...args) => {

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/backend/app/mongo.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/app/mongo.js b/modules/web-console/backend/app/mongo.js
index 81076af..e0d0a0f 100644
--- a/modules/web-console/backend/app/mongo.js
+++ b/modules/web-console/backend/app/mongo.js
@@ -79,6 +79,7 @@ const defineSchema = (passportMongo, mongoose) => {
         DUPLICATE_KEY_ERROR: 11000,
         DUPLICATE_KEY_UPDATE_ERROR: 11001
     };
+
     // Define Account model.
     result.Account = mongoose.model('Account', AccountSchema);
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/backend/package.json
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/package.json b/modules/web-console/backend/package.json
index f0b2b5e..ba442f9 100644
--- a/modules/web-console/backend/package.json
+++ b/modules/web-console/backend/package.json
@@ -68,7 +68,8 @@
     "passport-local": "1.0.0",
     "passport-local-mongoose": "4.0.0",
     "passport.socketio": "3.7.0",
-    "socket.io": "1.7.3"
+    "socket.io": "1.7.3",
+    "uuid": "3.1.0"
   },
   "devDependencies": {
     "chai": "4.1.0",

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/app.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/app.js b/modules/web-console/frontend/app/app.js
index ca678fc..f367d3e 100644
--- a/modules/web-console/frontend/app/app.js
+++ b/modules/web-console/frontend/app/app.js
@@ -22,7 +22,6 @@ import './app.config';
 
 import './modules/form/form.module';
 import './modules/agent/agent.module';
-import './modules/sql/sql.module';
 import './modules/nodes/nodes.module';
 import './modules/demo/Demo.module';
 
@@ -113,7 +112,6 @@ import resetPassword from './controllers/reset-password.controller';
 // Components
 import igniteListOfRegisteredUsers from './components/list-of-registered-users';
 import IgniteActivitiesUserDialog from './components/activities-user-dialog';
-import clusterSelect from './components/cluster-select';
 import './components/input-dialog';
 import webConsoleHeader from './components/web-console-header';
 import webConsoleFooter from './components/web-console-footer';
@@ -123,12 +121,14 @@ import userNotifications from './components/user-notifications';
 import pageConfigure from './components/page-configure';
 import pageConfigureBasic from './components/page-configure-basic';
 import pageConfigureAdvanced from './components/page-configure-advanced';
+import pageQueries from './components/page-queries';
 import gridColumnSelector from './components/grid-column-selector';
 import gridItemSelected from './components/grid-item-selected';
 import bsSelectMenu from './components/bs-select-menu';
 import protectFromBsSelectRender from './components/protect-from-bs-select-render';
 import uiGridHovering from './components/ui-grid-hovering';
 import listEditable from './components/list-editable';
+import clusterSelector from './components/cluster-selector';
 
 import igniteServices from './services';
 
@@ -168,7 +168,6 @@ angular.module('ignite-console', [
     'ignite-console.branding',
     'ignite-console.socket',
     'ignite-console.agent',
-    'ignite-console.sql',
     'ignite-console.nodes',
     'ignite-console.demo',
     // States.
@@ -197,6 +196,7 @@ angular.module('ignite-console', [
     pageConfigure.name,
     pageConfigureBasic.name,
     pageConfigureAdvanced.name,
+    pageQueries.name,
     gridColumnSelector.name,
     gridItemSelected.name,
     bsSelectMenu.name,
@@ -205,6 +205,7 @@ angular.module('ignite-console', [
     AngularStrapTooltip.name,
     AngularStrapSelect.name,
     listEditable.name,
+    clusterSelector.name,
     // Ignite modules.
     IgniteModules.name
 ])
@@ -231,7 +232,6 @@ angular.module('ignite-console', [
 .directive('igniteOnFocusOut', igniteOnFocusOut)
 .directive('igniteRestoreInputFocus', igniteRestoreInputFocus)
 .directive('igniteListOfRegisteredUsers', igniteListOfRegisteredUsers)
-.directive('igniteClusterSelect', clusterSelect)
 .directive('btnIgniteLinkDashedSuccess', btnIgniteLink)
 .directive('btnIgniteLinkDashedSecondary', btnIgniteLink)
 // Services.

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/bs-select-menu/style.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/bs-select-menu/style.scss b/modules/web-console/frontend/app/components/bs-select-menu/style.scss
index 870b1bf..ccf33a3 100644
--- a/modules/web-console/frontend/app/components/bs-select-menu/style.scss
+++ b/modules/web-console/frontend/app/components/bs-select-menu/style.scss
@@ -88,7 +88,7 @@
         }
 
         & > li > .bssm-item-button__active {
-            background-color: #eeeeee;
+            background-color: #e5f2f9;
         }
     }
-}
\ No newline at end of file
+}

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

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/cluster-select/cluster-select.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/cluster-select/cluster-select.pug b/modules/web-console/frontend/app/components/cluster-select/cluster-select.pug
deleted file mode 100644
index eb46e26..0000000
--- a/modules/web-console/frontend/app/components/cluster-select/cluster-select.pug
+++ /dev/null
@@ -1,47 +0,0 @@
-//-
-    Licensed to the Apache Software Foundation (ASF) under one or more
-    contributor license agreements.  See the NOTICE file distributed with
-    this work for additional information regarding copyright ownership.
-    The ASF licenses this file to You under the Apache License, Version 2.0
-    (the "License"); you may not use this file except in compliance with
-    the License.  You may obtain a copy of the License at
-
-         http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
-
--var clusterName = 'Cluster {{ ctrl.cluster.id | id8 }}'
-
-ul.nav
-    li.disabled(ng-if='ctrl.clusters.length === 0')
-        a(ng-if='!ctrl.cluster')
-            i.icon-cluster
-            label.padding-left-dflt(bs-tooltip='' data-placement='bottom' data-title='Check that Web Agent(s) started and connected to cluster(s)') No clusters available
-        a(ng-if='ctrl.cluster')
-            i.icon-danger
-            label.padding-left-dflt(bs-tooltip='' data-placement='bottom' data-title='Connection to cluster was lost') #{clusterName}
-
-    li(ng-if='ctrl.clusters.length === 1 && ctrl.cluster.connected')
-        a
-            i.icon-cluster
-            label.padding-left-dflt #{clusterName}
-
-    li(ng-if='ctrl.clusters.length > 1 || ctrl.clusters.length === 1 && !ctrl.cluster.connected')
-        a.dropdown-toggle(bs-dropdown='' data-placement='bottom-left' data-trigger='hover focus' data-container='self' ng-click='$event.stopPropagation()' aria-haspopup='true' aria-expanded='expanded')
-            i(ng-class='{"icon-cluster": ctrl.cluster.connected, "icon-danger": !ctrl.cluster.connected}')
-            label.padding-left-dflt #{clusterName}
-            span.caret
-            
-        ul.dropdown-menu(role='menu')
-            li(ng-repeat='item in ctrl.clusters' ng-class='{active: ctrl.cluster === item}')
-                div(ng-click='item.click()')
-                    i.icon-cluster.pull-left(style='margin: 0; padding-left: 10px;')
-                    div: a Cluster {{ item.id | id8 }}
-
-i.icon-help(bs-tooltip='' data-placement='bottom' data-html=true
-    data-title='Multi-Cluster Support<br/>\
-        <a href="https://apacheignite-tools.readme.io/docs/multi-cluster-support" target="_blank">More info</a>')

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/cluster-select/cluster-select.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/cluster-select/cluster-select.scss b/modules/web-console/frontend/app/components/cluster-select/cluster-select.scss
deleted file mode 100644
index 189ef50..0000000
--- a/modules/web-console/frontend/app/components/cluster-select/cluster-select.scss
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-ignite-cluster-select {
-    @import "./../../../public/stylesheets/variables.scss";
-
-    display: flex;
-    flex-direction: row;
-    align-items: center;
-
-    .icon-help {
-        margin-left: 4px;
-
-        color: $text-color;
-    }
-}

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

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

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/cluster-selector/controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/cluster-selector/controller.js b/modules/web-console/frontend/app/components/cluster-selector/controller.js
new file mode 100644
index 0000000..6a86357
--- /dev/null
+++ b/modules/web-console/frontend/app/components/cluster-selector/controller.js
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class {
+    static $inject = ['$scope', 'AgentManager', 'IgniteConfirm'];
+
+    constructor($scope, agentMgr, Confirm) {
+        Object.assign(this, { $scope, agentMgr, Confirm });
+
+        this.clusters = [];
+        this.isDemo = agentMgr.isDemoMode();
+    }
+
+    $onInit() {
+        this.clusters$ = this.agentMgr.connectionSbj
+            .do(({ cluster, clusters }) => {
+                this.cluster = cluster;
+                this.clusters = clusters;
+            })
+            .subscribe(() => {});
+    }
+
+    $onDestroy() {
+        this.clusters$.unsubscribe();
+    }
+
+    change() {
+        this.agentMgr.switchCluster(this.cluster);
+    }
+
+    toggle($event) {
+        $event.preventDefault();
+
+        const toggleClusterState = () => {
+            this.inProgress = true;
+
+            return this.agentMgr.toggleClusterState()
+                .finally(() => this.inProgress = false);
+        };
+
+        if (this.cluster.active) {
+            return this.Confirm.confirm('Are you sure you want to deactivate cluster?')
+                .then(() => toggleClusterState());
+        }
+
+        return toggleClusterState();
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/cluster-selector/index.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/cluster-selector/index.js b/modules/web-console/frontend/app/components/cluster-selector/index.js
new file mode 100644
index 0000000..2bdbe44
--- /dev/null
+++ b/modules/web-console/frontend/app/components/cluster-selector/index.js
@@ -0,0 +1,23 @@
+/*
+ * 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 angular from 'angular';
+import component from './component';
+
+export default angular
+    .module('ignite-console.cluster-selector', [])
+    .component('clusterSelector', component);

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/cluster-selector/style.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/cluster-selector/style.scss b/modules/web-console/frontend/app/components/cluster-selector/style.scss
new file mode 100644
index 0000000..966be99
--- /dev/null
+++ b/modules/web-console/frontend/app/components/cluster-selector/style.scss
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+
+cluster-selector {
+    @import "./../../../public/stylesheets/variables.scss";
+
+    position: relative;
+    top: 2px;
+
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+	& > .btn-ignite {
+        border-radius: 9px;
+        min-height: 0;
+        font-size: 12px;
+        font-weight: bold;
+        line-height: 17px;
+        padding-top: 0;
+        padding-bottom: 0;
+
+        button {
+            font-weight: normal;
+            margin: 0 !important;
+        }
+    }
+
+    .cluster-selector--state {
+        width: 85px;
+    }
+
+    div {
+        margin: 0 10px 0 20px;
+        font-family: Roboto;
+        font-size: 12px;
+    }
+
+    div:last-child {
+        margin-left: 10px;
+        color: #EE2B27;
+    }
+
+    [ignite-icon='info'] {
+        margin-left: 7px;
+        color: $ignite-brand-success;
+    }
+
+    .bs-select-menu {
+        color: $text-color;
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/cluster-selector/template.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/cluster-selector/template.pug b/modules/web-console/frontend/app/components/cluster-selector/template.pug
new file mode 100644
index 0000000..c97a698
--- /dev/null
+++ b/modules/web-console/frontend/app/components/cluster-selector/template.pug
@@ -0,0 +1,75 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+button.btn-ignite.btn-ignite--success(
+    data-ng-if='$ctrl.isDemo'
+)
+    | Demo cluster
+
+button.btn-ignite.btn-ignite--primary(
+    data-ng-if='!$ctrl.isDemo && $ctrl.clusters.length == 0'
+)
+    | No clusters available
+
+button.btn-ignite.btn-ignite--primary(
+    data-ng-if='!$ctrl.isDemo && $ctrl.clusters.length == 1'
+)
+    | {{ $ctrl.cluster.name }}
+
+div.btn-ignite.btn-ignite--primary(
+    data-ng-if='!$ctrl.isDemo && $ctrl.clusters.length > 1'
+
+    data-ng-model='$ctrl.cluster'
+
+    bs-select=''
+    bs-options='item as item.name for item in $ctrl.clusters'
+    data-trigger='hover focus'
+    data-container='self'
+
+    data-ng-change='$ctrl.change()'
+
+    protect-from-bs-select-render
+)
+    span(ng-if='!$ctrl.cluster') No clusters available
+    span(ng-if='$ctrl.cluster') {{ $ctrl.cluster.name }}
+        span.icon-right.fa.fa-caret-down
+
+svg(
+    ng-if='!$ctrl.isDemo'
+    ignite-icon='info'
+    bs-tooltip=''
+    data-title='Multi-Cluster Support<br/>\
+            <a href="https://apacheignite-tools.readme.io/docs/multi-cluster-support" target="_blank">More info</a>'
+    data-placement='bottom'
+)
+
+.cluster-selector--state(ng-if='!$ctrl.isDemo && $ctrl.cluster')
+    | Cluster {{ $ctrl.cluster.active ? 'active' : 'inactive' }}
+
++switcher()(
+    ng-if='!$ctrl.isDemo && $ctrl.cluster'
+    ng-click='$ctrl.toggle($event)'
+    ng-checked='$ctrl.cluster.active'
+    ng-disabled='$ctrl.inProgress'
+
+    tip='Toggle cluster active state'
+    is-in-progress='{{ $ctrl.inProgress }}'
+)
+
+div(ng-if='$ctrl.inProgress')
+    | {{ !$ctrl.cluster.active ? 'Activating...' : 'Deactivating...' }}

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/list-editable/controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/list-editable/controller.js b/modules/web-console/frontend/app/components/list-editable/controller.js
index bc864ce..7757d96 100644
--- a/modules/web-console/frontend/app/components/list-editable/controller.js
+++ b/modules/web-console/frontend/app/components/list-editable/controller.js
@@ -21,7 +21,7 @@ export default class {
     static $inject = ['$animate', '$element', '$transclude'];
 
     constructor($animate, $element, $transclude) {
-        $animate.enabled(false, $element);
+        $animate.enabled($element, false);
 
         this.hasItemView = $transclude.isSlotFilled('itemView');
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/page-queries/Notebook.data.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-queries/Notebook.data.js b/modules/web-console/frontend/app/components/page-queries/Notebook.data.js
new file mode 100644
index 0000000..3f98bed
--- /dev/null
+++ b/modules/web-console/frontend/app/components/page-queries/Notebook.data.js
@@ -0,0 +1,168 @@
+/*
+ * 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.
+ */
+
+const DEMO_NOTEBOOK = {
+    name: 'SQL demo',
+    paragraphs: [
+        {
+            name: 'Query with refresh rate',
+            cacheName: 'CarCache',
+            pageSize: 100,
+            limit: 0,
+            query: [
+                'SELECT count(*)',
+                'FROM "CarCache".Car'
+            ].join('\n'),
+            result: 'bar',
+            timeLineSpan: '1',
+            rate: {
+                value: 3,
+                unit: 1000,
+                installed: true
+            }
+        },
+        {
+            name: 'Simple query',
+            cacheName: 'CarCache',
+            pageSize: 100,
+            limit: 0,
+            query: 'SELECT * FROM "CarCache".Car',
+            result: 'table',
+            timeLineSpan: '1',
+            rate: {
+                value: 30,
+                unit: 1000,
+                installed: false
+            }
+        },
+        {
+            name: 'Query with aggregates',
+            cacheName: 'ParkingCache',
+            pageSize: 100,
+            limit: 0,
+            query: [
+                'SELECT p.name, count(*) AS cnt',
+                'FROM "ParkingCache".Parking p',
+                'INNER JOIN "CarCache".Car c',
+                '  ON (p.id) = (c.parkingId)',
+                'GROUP BY P.NAME'
+            ].join('\n'),
+            result: 'table',
+            timeLineSpan: '1',
+            rate: {
+                value: 30,
+                unit: 1000,
+                installed: false
+            }
+        }
+    ],
+    expandedParagraphs: [0, 1, 2]
+};
+
+export default class NotebookData {
+    static $inject = ['$rootScope', '$http', '$q'];
+
+    constructor($root, $http, $q) {
+        this.demo = $root.IgniteDemoMode;
+
+        this.initLatch = null;
+        this.notebooks = null;
+
+        this.$http = $http;
+        this.$q = $q;
+    }
+
+    load() {
+        if (this.demo) {
+            if (this.initLatch)
+                return this.initLatch;
+
+            return this.initLatch = this.$q.when(this.notebooks = [DEMO_NOTEBOOK]);
+        }
+
+        return this.initLatch = this.$http.get('/api/v1/notebooks')
+            .then(({data}) => this.notebooks = data)
+            .catch(({data}) => Promise.reject(data));
+    }
+
+    read() {
+        if (this.initLatch)
+            return this.initLatch;
+
+        return this.load();
+    }
+
+    find(_id) {
+        return this.read()
+            .then(() => {
+                const notebook = this.demo ? this.notebooks[0] : _.find(this.notebooks, {_id});
+
+                if (_.isNil(notebook))
+                    return this.$q.reject('Failed to load notebook.');
+
+                return notebook;
+            });
+    }
+
+    findIndex(notebook) {
+        return this.read()
+            .then(() => _.findIndex(this.notebooks, {_id: notebook._id}));
+    }
+
+    save(notebook) {
+        if (this.demo)
+            return this.$q.when(DEMO_NOTEBOOK);
+
+        return this.$http.post('/api/v1/notebooks/save', notebook)
+            .then(({data}) => {
+                const idx = _.findIndex(this.notebooks, {_id: data._id});
+
+                if (idx >= 0)
+                    this.notebooks[idx] = data;
+                else
+                    this.notebooks.push(data);
+
+                return data;
+            })
+            .catch(({data}) => Promise.reject(data));
+    }
+
+    remove(notebook) {
+        if (this.demo)
+            return this.$q.reject(`Removing "${notebook.name}" notebook is not supported.`);
+
+        const key = {_id: notebook._id};
+
+        return this.$http.post('/api/v1/notebooks/remove', key)
+            .then(() => {
+                const idx = _.findIndex(this.notebooks, key);
+
+                if (idx >= 0) {
+                    this.notebooks.splice(idx, 1);
+
+                    if (idx < this.notebooks.length)
+                        return this.notebooks[idx];
+                }
+
+                if (this.notebooks.length > 0)
+                    return this.notebooks[this.notebooks.length - 1];
+
+                return null;
+            })
+            .catch(({data}) => Promise.reject(data));
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/components/page-queries/Notebook.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-queries/Notebook.service.js b/modules/web-console/frontend/app/components/page-queries/Notebook.service.js
new file mode 100644
index 0000000..b0bb64f
--- /dev/null
+++ b/modules/web-console/frontend/app/components/page-queries/Notebook.service.js
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export default class Notebook {
+    static $inject = ['$state', 'IgniteConfirm', 'IgniteMessages', 'IgniteNotebookData'];
+
+    /**
+     * @param $state
+     * @param confirmModal
+     * @param Messages
+     * @param {NotebookData} NotebookData
+     */
+    constructor($state, confirmModal, Messages, NotebookData) {
+        this.$state = $state;
+        this.confirmModal = confirmModal;
+        this.Messages = Messages;
+        this.NotebookData = NotebookData;
+    }
+
+    read() {
+        return this.NotebookData.read();
+    }
+
+    create(name) {
+        return this.NotebookData.save({name});
+    }
+
+    save(notebook) {
+        return this.NotebookData.save(notebook);
+    }
+
+    find(_id) {
+        return this.NotebookData.find(_id);
+    }
+
+    _openNotebook(idx) {
+        return this.NotebookData.read()
+            .then((notebooks) => {
+                const nextNotebook = notebooks.length > idx ? notebooks[idx] : _.last(notebooks);
+
+                if (nextNotebook)
+                    this.$state.go('base.sql.notebook', {noteId: nextNotebook._id});
+                else
+                    this.$state.go('base.configuration.tabs.advanced.clusters');
+            });
+    }
+
+    remove(notebook) {
+        return this.confirmModal.confirm(`Are you sure you want to remove notebook: "${notebook.name}"?`)
+            .then(() => this.NotebookData.findIndex(notebook))
+            .then((idx) => {
+                this.NotebookData.remove(notebook)
+                    .then(() => {
+                        if (this.$state.includes('base.sql.notebook') && this.$state.params.noteId === notebook._id)
+                            return this._openNotebook(idx);
+                    })
+                    .catch(this.Messages.showError);
+            });
+    }
+}


[2/5] ignite git commit: IGNITE-6390 Web Console: Added component for cluster selection.

Posted by ak...@apache.org.
http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/modules/sql/sql.controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/sql/sql.controller.js b/modules/web-console/frontend/app/modules/sql/sql.controller.js
deleted file mode 100644
index a2ad912..0000000
--- a/modules/web-console/frontend/app/modules/sql/sql.controller.js
+++ /dev/null
@@ -1,1887 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-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';
-
-// 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 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 {
-    constructor($animate, $timeout, JavaTypes, 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 = err.message;
-
-            let cause = err;
-
-            while (_.nonNil(cause)) {
-                if (_.nonEmpty(cause.className) &&
-                    _.includes(['SQLException', 'JdbcSQLException', 'QueryCancelledException'], JavaTypes.shortClassName(cause.className))) {
-                    this.error.message = 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);
-    }
-}
-
-// Controller for SQL notebook screen.
-export default ['$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',
-    function($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, IgniteCopyToClipboard) {
-        const $ctrl = this;
-
-        // Define template urls.
-        $ctrl.paragraphRateTemplateUrl = paragraphRateTemplateUrl;
-        $ctrl.cacheMetadataTemplateUrl = cacheMetadataTemplateUrl;
-        $ctrl.chartSettingsTemplateUrl = chartSettingsTemplateUrl;
-
-        $ctrl.demoStarted = false;
-
-        let stopTopology = null;
-
-        const _tryStopRefresh = function(paragraph) {
-            if (paragraph.rate && paragraph.rate.stopTime) {
-                $interval.cancel(paragraph.rate.stopTime);
-
-                delete paragraph.rate.stopTime;
-            }
-        };
-
-        const _stopTopologyRefresh = () => {
-            $interval.cancel(stopTopology);
-
-            if ($scope.notebook && $scope.notebook.paragraphs)
-                $scope.notebook.paragraphs.forEach((paragraph) => _tryStopRefresh(paragraph));
-        };
-
-        $scope.$on('$stateChangeStart', _stopTopologyRefresh);
-
-        $scope.caches = [];
-
-        $scope.pageSizes = [50, 100, 200, 400, 800, 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: -25}
-                        }
-                    }
-                };
-
-                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: -25
-                                }
-                            }
-                        },
-                        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: -25
-                            }
-                        }
-                    }
-                };
-
-                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: -25}
-                        }
-                    }
-                };
-
-                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 _refreshFn = () =>
-            agentMgr.topology(true)
-                .then((nodes) => {
-                    $scope.caches = _.sortBy(_.reduce(nodes, (cachesAcc, node) => {
-                        _.forEach(node.caches, (cache) => {
-                            let item = _.find(cachesAcc, {name: cache.name});
-
-                            if (_.isNil(item)) {
-                                cache.label = maskCacheName(cache.name, true);
-                                cache.value = cache.name;
-
-                                cache.nodes = [];
-
-                                cachesAcc.push(item = cache);
-                            }
-
-                            item.nodes.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 cachesAcc;
-                    }, []), (cache) => cache.label.toLowerCase());
-
-                    // Reset to first cache in case of stopped selected.
-                    const cacheNames = _.map($scope.caches, (cache) => cache.value);
-
-                    _.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));
-                    }
-                })
-                .catch((err) => Messages.showError(err));
-
-        const _startWatch = () =>
-            agentMgr.startClusterWatch('Back to Configuration', 'base.configuration.tabs.advanced.clusters')
-                .then(() => Loading.start('sqlLoading'))
-                .then(_refreshFn)
-                .then(() => {
-                    if (!$root.IgniteDemoMode)
-                        Loading.finish('sqlLoading');
-                })
-                .then(() => {
-                    stopTopology = $interval(_refreshFn, 5000, 0, false);
-                });
-
-        Notebook.find($state.params.noteId)
-            .then((notebook) => {
-                $scope.notebook = _.cloneDeep(notebook);
-
-                $scope.notebook_name = $scope.notebook.name;
-
-                if (!$scope.notebook.expandedParagraphs)
-                    $scope.notebook.expandedParagraphs = [];
-
-                if (!$scope.notebook.paragraphs)
-                    $scope.notebook.paragraphs = [];
-
-                $scope.notebook.paragraphs = _.map($scope.notebook.paragraphs,
-                    (paragraph) => new Paragraph($animate, $timeout, JavaTypes, paragraph));
-
-                if (_.isEmpty($scope.notebook.paragraphs))
-                    $scope.addQuery();
-                else
-                    $scope.rebuildScrollParagraphs();
-            })
-            .then(_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.renameParagraph = function(paragraph, newName) {
-            if (!newName)
-                return;
-
-            if (paragraph.name !== newName) {
-                paragraph.name = newName;
-
-                $scope.rebuildScrollParagraphs();
-
-                Notebook.save($scope.notebook)
-                    .then(() => paragraph.edit = false)
-                    .catch(Messages.showError);
-            }
-            else
-                paragraph.edit = false;
-        };
-
-        $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({ action: '/queries/add/query' });
-
-            const paragraph = new Paragraph($animate, $timeout, JavaTypes, {
-                name: 'Query' + (sz === 0 ? '' : sz),
-                query: '',
-                pageSize: $scope.pageSizes[1],
-                timeLineSpan: $scope.timeLineSpans[0],
-                result: 'none',
-                rate: {
-                    value: 1,
-                    unit: 60000,
-                    installed: false
-                },
-                qryType: 'query'
-            });
-
-            $scope.addParagraph(paragraph, sz);
-
-            $timeout(() => {
-                $anchorScroll();
-
-                paragraph.ace.focus();
-            });
-        };
-
-        $scope.addScan = function() {
-            const sz = $scope.notebook.paragraphs.length;
-
-            ActivitiesData.post({ action: '/queries/add/scan' });
-
-            const paragraph = new Paragraph($animate, $timeout, JavaTypes, {
-                name: 'Scan' + (sz === 0 ? '' : sz),
-                query: '',
-                pageSize: $scope.pageSizes[1],
-                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.removeParagraph = function(paragraph) {
-            Confirm.confirm('Are you sure you want to remove query: "' + paragraph.name + '"?')
-                .then(function() {
-                    $scope.stopRefresh(paragraph);
-
-                    const paragraph_idx = _.findIndex($scope.notebook.paragraphs, function(item) {
-                        return paragraph === item;
-                    });
-
-                    const panel_idx = _.findIndex($scope.expandedParagraphs, function(item) {
-                        return paragraph_idx === item;
-                    });
-
-                    if (panel_idx >= 0)
-                        $scope.expandedParagraphs.splice(panel_idx, 1);
-
-                    $scope.notebook.paragraphs.splice(paragraph_idx, 1);
-
-                    $scope.rebuildScrollParagraphs();
-
-                    Notebook.save($scope.notebook)
-                        .catch(Messages.showError);
-                });
-        };
-
-        $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.
-         * @return {Array.<String>} Nids
-         */
-        const cacheNodes = (name) => {
-            return _.find($scope.caches, {name}).nodes;
-        };
-
-        /**
-         * @param {String} name Cache name.
-         * @param {Boolean} local Local query.
-         * @return {String} Nid
-         */
-        const _chooseNode = (name, local) => {
-            if (_.isEmpty(name))
-                return Promise.resolve(null);
-
-            const nodes = _.filter(cacheNodes(name), (node) => !node.client);
-
-            if (local) {
-                return Nodes.selectNode(nodes, name)
-                    .then((selectedNids) => _.head(selectedNids));
-            }
-
-            return Promise.resolve(nodes[_.random(0, nodes.length - 1)].nid);
-        };
-
-        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))
-                .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 = (paragraph) => {
-            const cache = _.find($scope.caches, {name: paragraph.cacheName});
-
-            if (cache)
-                return !!_.find(cache.nodes, (node) => Version.since(node.version, NON_COLLOCATED_JOINS_SINCE));
-
-            return false;
-        };
-
-        $scope.enforceJoinOrderAvailable = (paragraph) => {
-            const cache = _.find($scope.caches, {name: paragraph.cacheName});
-
-            if (cache)
-                return !!_.find(cache.nodes, (node) => Version.since(node.version, ...ENFORCE_JOIN_SINCE));
-
-            return false;
-        };
-
-        $scope.lazyQueryAvailable = (paragraph) => {
-            const cache = _.find($scope.caches, {name: paragraph.cacheName});
-
-            if (cache)
-                return !!_.find(cache.nodes, (node) => Version.since(node.version, ...LAZY_QUERY_SINCE));
-
-            return false;
-        };
-
-        $scope.ddlAvailable = (paragraph) => {
-            const cache = _.find($scope.caches, {name: paragraph.cacheName});
-
-            if (cache)
-                return !!_.find(cache.nodes, (node) => Version.since(node.version, ...DDL_SINCE));
-
-            return false;
-        };
-
-        $scope.execute = (paragraph, local = false) => {
-            const nonCollocatedJoins = !!paragraph.nonCollocatedJoins;
-            const enforceJoinOrder = !!paragraph.enforceJoinOrder;
-            const lazy = !!paragraph.lazy;
-
-            $scope.queryAvailable(paragraph) && _chooseNode(paragraph.cacheName, local)
-                .then((nid) => {
-                    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 args = paragraph.queryArgs = {
-                                type: 'QUERY',
-                                cacheName: ($scope.ddlAvailable(paragraph) && !paragraph.useAsDefaultSchema) ? null : paragraph.cacheName,
-                                query: paragraph.query,
-                                pageSize: paragraph.pageSize,
-                                maxPages: paragraph.maxPages,
-                                nonCollocatedJoins,
-                                enforceJoinOrder,
-                                localNid: local ? nid : null,
-                                lazy
-                            };
-
-                            const qry = args.maxPages ? addLimit(args.query, args.pageSize * args.maxPages) : paragraph.query;
-
-                            ActivitiesData.post({ action: '/queries/execute' });
-
-                            return agentMgr.querySql(nid, args.cacheName, qry, nonCollocatedJoins, enforceJoinOrder, false, local, args.pageSize, lazy);
-                        })
-                        .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) => {
-            if (!$scope.queryAvailable(paragraph))
-                return;
-
-            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: paragraph.cacheName,
-                        query: 'EXPLAIN ' + paragraph.query,
-                        pageSize: paragraph.pageSize
-                    };
-
-                    ActivitiesData.post({ action: '/queries/explain' });
-
-                    return agentMgr.querySql(nid, args.cacheName, args.query, false, !!paragraph.enforceJoinOrder, false, false, args.pageSize, false);
-                })
-                .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;
-
-            paragraph.localQueryMode = local;
-
-            $scope.scanAvailable(paragraph) && _chooseNode(cacheName, local)
-                .then((nid) => {
-                    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({ 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) => {
-            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(';') + '\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(';') + '\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))
-                .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, { name: 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)) {
-                        const clsName = _.isEmpty(item.className) ? '' : '[' + JavaTypes.shortClassName(item.className) + '] ';
-
-                        scope.content.push((scope.content.length > 0 ? tab : '') + clsName + (item.message || ''));
-
-                        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});
-            }
-        };
-    }
-];

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/modules/sql/sql.module.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/sql/sql.module.js b/modules/web-console/frontend/app/modules/sql/sql.module.js
deleted file mode 100644
index da9955c..0000000
--- a/modules/web-console/frontend/app/modules/sql/sql.module.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import angular from 'angular';
-
-import NotebookData from './Notebook.data';
-import Notebook from './Notebook.service';
-import notebook from './notebook.controller';
-import controller from './sql.controller';
-
-import sqlTplUrl from 'app/../views/sql/sql.tpl.pug';
-
-angular.module('ignite-console.sql', [
-    'ui.router'
-])
-.config(['$stateProvider', ($stateProvider) => {
-    // set up the states
-    $stateProvider
-        .state('base.sql', {
-            url: '/queries',
-            abstract: true,
-            template: '<ui-view></ui-view>'
-        })
-        .state('base.sql.notebook', {
-            url: '/notebook/{noteId}',
-            templateUrl: sqlTplUrl,
-            permission: 'query',
-            tfMetaTags: {
-                title: 'Query notebook'
-            },
-            controller,
-            controllerAs: '$ctrl'
-        })
-        .state('base.sql.demo', {
-            url: '/demo',
-            templateUrl: sqlTplUrl,
-            permission: 'query',
-            tfMetaTags: {
-                title: 'SQL demo'
-            },
-            controller,
-            controllerAs: '$ctrl'
-        });
-}])
-.service('IgniteNotebookData', NotebookData)
-.service('IgniteNotebook', Notebook)
-.controller('notebookController', notebook);

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/primitives/switcher/index.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/switcher/index.pug b/modules/web-console/frontend/app/primitives/switcher/index.pug
index 5689094..8b7d009 100644
--- a/modules/web-console/frontend/app/primitives/switcher/index.pug
+++ b/modules/web-console/frontend/app/primitives/switcher/index.pug
@@ -17,4 +17,4 @@
 mixin switcher()
     label.switcher--ignite
         input(type='checkbox')&attributes(attributes)
-        div(bs-tooltip=attributes.tip && '' data-title=attributes.tip data-trigger='hover')
+        div(bs-tooltip=attributes.tip && '' data-title=attributes.tip data-trigger='hover' data-placement='bottom')

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/app/primitives/switcher/index.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/switcher/index.scss b/modules/web-console/frontend/app/primitives/switcher/index.scss
index 3e9cd49..fb2fd1b 100644
--- a/modules/web-console/frontend/app/primitives/switcher/index.scss
+++ b/modules/web-console/frontend/app/primitives/switcher/index.scss
@@ -15,31 +15,39 @@
  * limitations under the License.
  */
 
-@import '../../../public/stylesheets/variables';
+@import 'public/stylesheets/variables';
 
 label.switcher--ignite {
-    width: 34px;
-    max-width: 34px !important;
-    height: 20px;
+    $width: 34px;
+    $height: 20px;
 
-    line-height: 20px;
+    $color-inactive-primary: #c5c5c5;
+    $color-inactive-secondary: #ffffff;
+    $color-active-primary: $ignite-brand-primary;
+    $color-active-secondary: #ff8485;
+
+    width: $width;
+    max-width: $width !important;
+    height: $height;
+
+    line-height: $height;
     vertical-align: middle;
 
     cursor: pointer;
 
-    input[type="checkbox"] {
+    input[type='checkbox'] {
         position: absolute;
         opacity: 0.0;
 
         & + div {
             position: relative;
 
-            width: 34px;
+            width: $width;
             height: 14px;
             margin: 3px 0;
 
             border-radius: 8px;
-            background-color: #C5C5C5;
+            background-color: $color-inactive-primary;
             transition: background 0.2s ease;
 
             &:before {
@@ -49,12 +57,14 @@ label.switcher--ignite {
                 top: -3px;
                 left: 0;
 
-                width: 20px;
-                height: 20px;
+                width: $height;
+                height: $height;
 
-                border: solid 1px #C5C5C5;
+                border-width: 1px;
+                border-style: solid;
                 border-radius: 50%;
-                background-color: #FFF;
+                border-color: $color-inactive-primary;
+                background-color: $color-inactive-secondary;
 
                 transition: all 0.12s ease;
             }
@@ -64,17 +74,46 @@ label.switcher--ignite {
             }
         }
 
+        &[is-in-progress='true'] + div:before {
+            border-left-width: 2px;
+            border-left-color: $color-active-primary;
+
+            animation-name: switcher--animation;
+            animation-duration: 1s;
+            animation-iteration-count: infinite;
+            animation-timing-function: linear;
+        }
+
         &:checked + div {
-            background-color: #FF8485;
+            background-color: $color-active-secondary;
 
             &:before {
                 content: '';
 
                 left: 14px;
 
-                border: 0;
-                background-color: #EE2B27;
+                border-color: $color-active-primary;
+                background-color: $color-active-primary;
+            }
+        }
+
+        &[is-in-progress='true']:checked + div {
+            background-color: $color-inactive-primary;
+
+            &:before {
+                border-color: $color-inactive-primary;
+                border-left-color: $color-active-primary;
+                background-color: $color-inactive-secondary;
             }
         }
     }
 }
+
+@keyframes switcher--animation {
+    from {
+        transform: rotate(0deg);
+    }
+    to {
+        transform: rotate(360deg);
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/1367bc98/modules/web-console/frontend/views/includes/header-right.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/views/includes/header-right.pug b/modules/web-console/frontend/views/includes/header-right.pug
index 8eeb281..56fd102 100644
--- a/modules/web-console/frontend/views/includes/header-right.pug
+++ b/modules/web-console/frontend/views/includes/header-right.pug
@@ -20,8 +20,6 @@
         ng-click='startDemo()'
     ) Start Demo
 
-ignite-cluster-select.wch-nav-item(ng-if='!IgniteDemoMode')
-
 .wch-nav-item(ignite-userbar)
     div(
         ng-class='{active: $state.includes("base.settings")}'