You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by sb...@apache.org on 2017/12/08 08:34:39 UTC
[08/30] ignite git commit: IGNITE-6390 Web Console: Added component
for cluster selection.
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 = ' ';
+
+ 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;
+ }
+}