blob: 372513a5e72c88f2e7cd270d9c14ba945d56c138 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import _ from 'lodash';
import {nonEmpty, nonNil} from 'app/utils/lodashMixins';
import id8 from 'app/utils/id8';
import {Subject, defer, from, of, merge, timer, EMPTY} from 'rxjs';
import {catchError, distinctUntilChanged, expand, exhaustMap, filter, finalize, first, ignoreElements, map, mergeMap, pluck, switchMap, takeUntil, takeWhile, take, tap} from 'rxjs/operators';
import {CSV} from 'app/services/CSV';
import paragraphRateTemplateUrl from 'views/sql/paragraph-rate.tpl.pug';
import cacheMetadataTemplateUrl from 'views/sql/cache-metadata.tpl.pug';
import chartSettingsTemplateUrl from 'views/sql/chart-settings.tpl.pug';
import messageTemplateUrl from 'views/templates/message.tpl.pug';
import {default as Notebook} from '../../notebook.service';
import {default as MessagesServiceFactory} from 'app/services/Messages.service';
import {default as LegacyConfirmServiceFactory} from 'app/services/Confirm.service';
import {default as InputDialog} from 'app/components/input-dialog/input-dialog.service';
import {QueryActions} from './components/query-actions-button/controller';
import {CancellationError} from 'app/errors/CancellationError';
// Time line X axis descriptor.
const TIME_LINE = {value: -1, type: 'java.sql.Date', label: 'TIME_LINE'};
// Row index X axis descriptor.
const ROW_IDX = {value: -2, type: 'java.lang.Integer', label: 'ROW_IDX'};
const NON_COLLOCATED_JOINS_SINCE = '1.7.0';
const COLLOCATED_QUERY_SINCE = [['2.3.5', '2.4.0'], ['2.4.6', '2.5.0'], ['2.5.1-p13', '2.6.0'], '2.7.0'];
const ENFORCE_JOIN_SINCE = [['1.7.9', '1.8.0'], ['1.8.4', '1.9.0'], '1.9.1'];
const LAZY_QUERY_SINCE = [['2.1.4-p1', '2.2.0'], '2.2.1'];
const DDL_SINCE = [['2.1.6', '2.2.0'], '2.3.0'];
const _fullColName = (col) => {
const res = [];
if (col.schemaName)
res.push(col.schemaName);
if (col.typeName)
res.push(col.typeName);
res.push(col.fieldName);
return res.join('.');
};
let paragraphId = 0;
class Paragraph {
name: string;
qryType: 'scan' | 'query';
constructor($animate, $timeout, JavaTypes, errorParser, paragraph) {
const self = this;
self.id = 'paragraph-' + paragraphId++;
self.qryType = paragraph.qryType || 'query';
self.maxPages = 0;
self.filter = '';
self.useAsDefaultSchema = false;
self.localQueryMode = false;
self.csvIsPreparing = false;
self.scanningInProgress = false;
self.cancelQuerySubject = new Subject();
self.cancelExportSubject = new Subject();
_.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.showLoading = (enable) => {
if (this.qryType === 'scan')
this.scanningInProgress = enable;
this.loading = enable;
};
this.setError = (err) => {
this.error.root = err;
this.error.message = errorParser.extractMessage(err);
let cause = err;
while (nonNil(cause)) {
if (nonEmpty(cause.className) &&
_.includes(['SQLException', 'JdbcSQLException', 'QueryCancelledException'], JavaTypes.shortClassName(cause.className))) {
this.error.message = errorParser.extractMessage(cause.message || cause.className);
break;
}
cause = cause.cause;
}
if (_.isEmpty(this.error.message) && nonEmpty(err.className)) {
this.error.message = 'Internal cluster error';
if (nonEmpty(err.className))
this.error.message += ': ' + err.className;
}
};
}
resultType() {
if (_.isNil(this.queryArgs))
return null;
if (nonEmpty(this.error.message))
return 'error';
if (_.isEmpty(this.rows))
return 'empty';
return this.result === 'table' ? 'table' : 'chart';
}
nonRefresh() {
return _.isNil(this.rate) || _.isNil(this.rate.stopTime);
}
table() {
return this.result === 'table';
}
chart() {
return this.result !== 'table' && this.result !== 'none';
}
nonEmpty() {
return this.rows && this.rows.length > 0;
}
queryExecuted() {
return nonEmpty(this.meta) || nonEmpty(this.error.message);
}
scanExplain() {
return this.queryExecuted() && (this.qryType === 'scan' || this.queryArgs.query.startsWith('EXPLAIN '));
}
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);
}
toJSON() {
return {
name: this.name,
query: this.query,
result: this.result,
pageSize: this.pageSize,
timeLineSpan: this.timeLineSpan,
maxPages: this.maxPages,
cacheName: this.cacheName,
useAsDefaultSchema: this.useAsDefaultSchema,
chartsOptions: this.chartsOptions,
rate: this.rate,
qryType: this.qryType,
nonCollocatedJoins: this.nonCollocatedJoins,
enforceJoinOrder: this.enforceJoinOrder,
lazy: this.lazy,
collocated: this.collocated
};
}
}
// Controller for SQL notebook screen.
export class NotebookCtrl {
static $inject = ['IgniteInput', '$rootScope', '$scope', '$http', '$q', '$timeout', '$transitions', '$interval', '$animate', '$location', '$anchorScroll', '$state', '$filter', '$modal', '$popover', '$window', 'IgniteLoading', 'IgniteLegacyUtils', 'IgniteMessages', 'IgniteConfirm', 'AgentManager', 'IgniteChartColors', 'IgniteNotebook', 'IgniteNodes', 'uiGridExporterConstants', 'IgniteVersion', 'IgniteActivitiesData', 'JavaTypes', 'IgniteCopyToClipboard', 'CSV', 'IgniteErrorParser', 'DemoInfo'];
/**
* @param {CSV} CSV
*/
constructor(private IgniteInput: InputDialog, $root, private $scope, $http, $q, $timeout, $transitions, $interval, $animate, $location, $anchorScroll, $state, $filter, $modal, $popover, $window, Loading, LegacyUtils, private Messages: ReturnType<typeof MessagesServiceFactory>, private Confirm: ReturnType<typeof LegacyConfirmServiceFactory>, agentMgr, IgniteChartColors, private Notebook: Notebook, Nodes, uiGridExporterConstants, Version, ActivitiesData, JavaTypes, IgniteCopyToClipboard, CSV, errorParser, DemoInfo) {
const $ctrl = this;
this.CSV = CSV;
Object.assign(this, { $root, $scope, $http, $q, $timeout, $transitions, $interval, $animate, $location, $anchorScroll, $state, $filter, $modal, $popover, $window, Loading, LegacyUtils, Messages, Confirm, agentMgr, IgniteChartColors, Notebook, Nodes, uiGridExporterConstants, Version, ActivitiesData, JavaTypes, errorParser, DemoInfo });
// Define template urls.
$ctrl.paragraphRateTemplateUrl = paragraphRateTemplateUrl;
$ctrl.cacheMetadataTemplateUrl = cacheMetadataTemplateUrl;
$ctrl.chartSettingsTemplateUrl = chartSettingsTemplateUrl;
$ctrl.demoStarted = false;
this.isDemo = $root.IgniteDemoMode;
const _tryStopRefresh = function(paragraph) {
paragraph.cancelRefresh($interval);
};
this._stopTopologyRefresh = () => {
if ($scope.notebook && $scope.notebook.paragraphs)
$scope.notebook.paragraphs.forEach((paragraph) => _tryStopRefresh(paragraph));
};
$scope.caches = [];
$scope.pageSizesOptions = [
{value: 50, label: '50'},
{value: 100, label: '100'},
{value: 200, label: '200'},
{value: 400, label: '400'},
{value: 800, label: '800'},
{value: 1000, label: '1000'}
];
$scope.maxPages = [
{label: 'Unlimited', value: 0},
{label: '1', value: 1},
{label: '5', value: 5},
{label: '10', value: 10},
{label: '20', value: 20},
{label: '50', value: 50},
{label: '100', value: 100}
];
$scope.timeLineSpans = ['1', '5', '10', '15', '30'];
$scope.aggregateFxs = ['FIRST', 'LAST', 'MIN', 'MAX', 'SUM', 'AVG', 'COUNT'];
$scope.modes = LegacyUtils.mkOptions(['PARTITIONED', 'REPLICATED', 'LOCAL']);
$scope.loadingText = $root.IgniteDemoMode ? 'Demo grid is starting. Please wait...' : 'Loading query notebook screen...';
$scope.timeUnit = [
{value: 1000, label: 'seconds', short: 's'},
{value: 60000, label: 'minutes', short: 'm'},
{value: 3600000, label: 'hours', short: 'h'}
];
$scope.metadata = [];
$scope.metaFilter = '';
$scope.metaOptions = {
nodeChildren: 'children',
dirSelectable: true,
injectClasses: {
iExpanded: 'fa fa-minus-square-o',
iCollapsed: 'fa fa-plus-square-o'
}
};
const maskCacheName = $filter('defaultName');
// We need max 1800 items to hold history for 30 mins in case of refresh every second.
const HISTORY_LENGTH = 1800;
const MAX_VAL_COLS = IgniteChartColors.length;
$anchorScroll.yOffset = 55;
$scope.chartColor = function(index) {
return {color: 'white', 'background-color': IgniteChartColors[index]};
};
function _chartNumber(arr, idx, dflt) {
if (idx >= 0 && arr && arr.length > idx && _.isNumber(arr[idx]))
return arr[idx];
return dflt;
}
function _min(rows, idx, dflt) {
let min = _chartNumber(rows[0], idx, dflt);
_.forEach(rows, (row) => {
const v = _chartNumber(row, idx, dflt);
if (v < min)
min = v;
});
return min;
}
function _max(rows, idx, dflt) {
let max = _chartNumber(rows[0], idx, dflt);
_.forEach(rows, (row) => {
const v = _chartNumber(row, idx, dflt);
if (v > max)
max = v;
});
return max;
}
function _sum(rows, idx) {
let sum = 0;
_.forEach(rows, (row) => sum += _chartNumber(row, idx, 0));
return sum;
}
function _aggregate(rows, aggFx, idx, dflt) {
const len = rows.length;
switch (aggFx) {
case 'FIRST':
return _chartNumber(rows[0], idx, dflt);
case 'LAST':
return _chartNumber(rows[len - 1], idx, dflt);
case 'MIN':
return _min(rows, idx, dflt);
case 'MAX':
return _max(rows, idx, dflt);
case 'SUM':
return _sum(rows, idx);
case 'AVG':
return len > 0 ? _sum(rows, idx) / len : 0;
case 'COUNT':
return len;
default:
}
return 0;
}
function _chartLabel(arr, idx, dflt) {
if (arr && arr.length > idx && _.isString(arr[idx]))
return arr[idx];
return dflt;
}
function _chartDatum(paragraph) {
let datum = [];
if (paragraph.chartColumnsConfigured()) {
paragraph.chartValCols.forEach(function(valCol) {
let index = 0;
let values = [];
const colIdx = valCol.value;
if (paragraph.chartTimeLineEnabled()) {
const aggFx = valCol.aggFx;
const colLbl = valCol.label + ' [' + aggFx + ']';
if (paragraph.charts && paragraph.charts.length === 1)
datum = paragraph.charts[0].data;
const chartData = _.find(datum, {series: valCol.label});
const leftBound = new Date();
leftBound.setMinutes(leftBound.getMinutes() - parseInt(paragraph.timeLineSpan, 10));
if (chartData) {
const lastItem = _.last(paragraph.chartHistory);
values = chartData.values;
values.push({
x: lastItem.tm,
y: _aggregate(lastItem.rows, aggFx, colIdx, index++)
});
while (values.length > 0 && values[0].x < leftBound)
values.shift();
}
else {
_.forEach(paragraph.chartHistory, (history) => {
if (history.tm >= leftBound) {
values.push({
x: history.tm,
y: _aggregate(history.rows, aggFx, colIdx, index++)
});
}
});
datum.push({series: valCol.label, key: colLbl, values});
}
}
else {
index = paragraph.total;
values = _.map(paragraph.rows, function(row) {
const xCol = paragraph.chartKeyCols[0].value;
const v = {
x: _chartNumber(row, xCol, index),
xLbl: _chartLabel(row, xCol, null),
y: _chartNumber(row, colIdx, index)
};
index++;
return v;
});
datum.push({series: valCol.label, key: valCol.label, values});
}
});
}
return datum;
}
function _xX(d) {
return d.x;
}
function _yY(d) {
return d.y;
}
function _xAxisTimeFormat(d) {
return d3.time.format('%X')(new Date(d));
}
const _intClasses = ['java.lang.Byte', 'java.lang.Integer', 'java.lang.Long', 'java.lang.Short'];
function _intType(cls) {
return _.includes(_intClasses, cls);
}
const _xAxisWithLabelFormat = function(paragraph) {
return function(d) {
const values = paragraph.charts[0].data[0].values;
const fmt = _intType(paragraph.chartKeyCols[0].type) ? 'd' : ',.2f';
const dx = values[d];
if (!dx)
return d3.format(fmt)(d);
const lbl = dx.xLbl;
return lbl ? lbl : d3.format(fmt)(d);
};
};
function _xAxisLabel(paragraph) {
return _.isEmpty(paragraph.chartKeyCols) ? 'X' : paragraph.chartKeyCols[0].label;
}
const _yAxisFormat = function(d) {
const fmt = d < 1000 ? ',.2f' : '.3s';
return d3.format(fmt)(d);
};
function _updateCharts(paragraph) {
$timeout(() => _.forEach(paragraph.charts, (chart) => chart.api.update()), 100);
}
function _updateChartsWithData(paragraph, newDatum) {
$timeout(() => {
if (!paragraph.chartTimeLineEnabled()) {
const chartDatum = paragraph.charts[0].data;
chartDatum.length = 0;
_.forEach(newDatum, (series) => chartDatum.push(series));
}
paragraph.charts[0].api.update();
});
}
function _yAxisLabel(paragraph) {
const cols = paragraph.chartValCols;
const tml = paragraph.chartTimeLineEnabled();
return _.isEmpty(cols) ? 'Y' : _.map(cols, function(col) {
let lbl = col.label;
if (tml)
lbl += ' [' + col.aggFx + ']';
return lbl;
}).join(', ');
}
function _barChart(paragraph) {
const datum = _chartDatum(paragraph);
if (_.isEmpty(paragraph.charts)) {
const stacked = paragraph.chartsOptions && paragraph.chartsOptions.barChart
? paragraph.chartsOptions.barChart.stacked
: true;
const options = {
chart: {
type: 'multiBarChart',
height: 400,
margin: {left: 70},
duration: 0,
x: _xX,
y: _yY,
xAxis: {
axisLabel: _xAxisLabel(paragraph),
tickFormat: paragraph.chartTimeLineEnabled() ? _xAxisTimeFormat : _xAxisWithLabelFormat(paragraph),
showMaxMin: false
},
yAxis: {
axisLabel: _yAxisLabel(paragraph),
tickFormat: _yAxisFormat
},
color: IgniteChartColors,
stacked,
showControls: true,
legend: {
vers: 'furious',
margin: {right: -15}
}
}
};
paragraph.charts = [{options, data: datum}];
_updateCharts(paragraph);
}
else
_updateChartsWithData(paragraph, datum);
}
function _pieChartDatum(paragraph) {
const datum = [];
if (paragraph.chartColumnsConfigured() && !paragraph.chartTimeLineEnabled()) {
paragraph.chartValCols.forEach(function(valCol) {
let index = paragraph.total;
const values = _.map(paragraph.rows, (row) => {
const xCol = paragraph.chartKeyCols[0].value;
const v = {
x: xCol < 0 ? index : row[xCol],
y: _chartNumber(row, valCol.value, index)
};
// Workaround for known problem with zero values on Pie chart.
if (v.y === 0)
v.y = 0.0001;
index++;
return v;
});
datum.push({series: paragraph.chartKeyCols[0].label, key: valCol.label, values});
});
}
return datum;
}
function _pieChart(paragraph) {
let datum = _pieChartDatum(paragraph);
if (datum.length === 0)
datum = [{values: []}];
paragraph.charts = _.map(datum, function(data) {
return {
options: {
chart: {
type: 'pieChart',
height: 400,
duration: 0,
x: _xX,
y: _yY,
showLabels: true,
labelThreshold: 0.05,
labelType: 'percent',
donut: true,
donutRatio: 0.35,
legend: {
vers: 'furious',
margin: {right: -15}
}
},
title: {
enable: true,
text: data.key
}
},
data: data.values
};
});
_updateCharts(paragraph);
}
function _lineChart(paragraph) {
const datum = _chartDatum(paragraph);
if (_.isEmpty(paragraph.charts)) {
const options = {
chart: {
type: 'lineChart',
height: 400,
margin: { left: 70 },
duration: 0,
x: _xX,
y: _yY,
xAxis: {
axisLabel: _xAxisLabel(paragraph),
tickFormat: paragraph.chartTimeLineEnabled() ? _xAxisTimeFormat : _xAxisWithLabelFormat(paragraph),
showMaxMin: false
},
yAxis: {
axisLabel: _yAxisLabel(paragraph),
tickFormat: _yAxisFormat
},
color: IgniteChartColors,
useInteractiveGuideline: true,
legend: {
vers: 'furious',
margin: {right: -15}
}
}
};
paragraph.charts = [{options, data: datum}];
_updateCharts(paragraph);
}
else
_updateChartsWithData(paragraph, datum);
}
function _areaChart(paragraph) {
const datum = _chartDatum(paragraph);
if (_.isEmpty(paragraph.charts)) {
const style = paragraph.chartsOptions && paragraph.chartsOptions.areaChart
? paragraph.chartsOptions.areaChart.style
: 'stack';
const options = {
chart: {
type: 'stackedAreaChart',
height: 400,
margin: {left: 70},
duration: 0,
x: _xX,
y: _yY,
xAxis: {
axisLabel: _xAxisLabel(paragraph),
tickFormat: paragraph.chartTimeLineEnabled() ? _xAxisTimeFormat : _xAxisWithLabelFormat(paragraph),
showMaxMin: false
},
yAxis: {
axisLabel: _yAxisLabel(paragraph),
tickFormat: _yAxisFormat
},
color: IgniteChartColors,
style,
legend: {
vers: 'furious',
margin: {right: -15}
}
}
};
paragraph.charts = [{options, data: datum}];
_updateCharts(paragraph);
}
else
_updateChartsWithData(paragraph, datum);
}
function _chartApplySettings(paragraph, resetCharts) {
if (resetCharts)
paragraph.charts = [];
if (paragraph.chart() && paragraph.nonEmpty()) {
switch (paragraph.result) {
case 'bar':
_barChart(paragraph);
break;
case 'pie':
_pieChart(paragraph);
break;
case 'line':
_lineChart(paragraph);
break;
case 'area':
_areaChart(paragraph);
break;
default:
}
}
}
$scope.chartRemoveKeyColumn = function(paragraph, index) {
paragraph.chartKeyCols.splice(index, 1);
_chartApplySettings(paragraph, true);
};
$scope.chartRemoveValColumn = function(paragraph, index) {
paragraph.chartValCols.splice(index, 1);
_chartApplySettings(paragraph, true);
};
$scope.chartAcceptKeyColumn = function(paragraph, item) {
const accepted = _.findIndex(paragraph.chartKeyCols, item) < 0;
if (accepted) {
paragraph.chartKeyCols = [item];
_chartApplySettings(paragraph, true);
}
return false;
};
const _numberClasses = ['java.math.BigDecimal', 'java.lang.Byte', 'java.lang.Double',
'java.lang.Float', 'java.lang.Integer', 'java.lang.Long', 'java.lang.Short'];
const _numberType = function(cls) {
return _.includes(_numberClasses, cls);
};
$scope.chartAcceptValColumn = function(paragraph, item) {
const valCols = paragraph.chartValCols;
const accepted = _.findIndex(valCols, item) < 0 && item.value >= 0 && _numberType(item.type);
if (accepted) {
if (valCols.length === MAX_VAL_COLS - 1)
valCols.shift();
valCols.push(item);
_chartApplySettings(paragraph, true);
}
return false;
};
$scope.scrollParagraphs = [];
$scope.rebuildScrollParagraphs = function() {
$scope.scrollParagraphs = $scope.notebook.paragraphs.map(function(paragraph) {
return {
text: paragraph.name,
click: 'scrollToParagraph("' + paragraph.id + '")'
};
});
};
$scope.scrollToParagraph = (id) => {
const idx = _.findIndex($scope.notebook.paragraphs, {id});
if (idx >= 0) {
if (!_.includes($scope.notebook.expandedParagraphs, idx))
$scope.notebook.expandedParagraphs = $scope.notebook.expandedParagraphs.concat([idx]);
if ($scope.notebook.paragraphs[idx].ace)
setTimeout(() => $scope.notebook.paragraphs[idx].ace.focus());
}
$location.hash(id);
$anchorScroll();
};
const _hideColumn = (col) => col.fieldName !== '_KEY' && col.fieldName !== '_VAL';
const _allColumn = () => true;
$scope.aceInit = function(paragraph) {
return function(editor) {
editor.setAutoScrollEditorIntoView(true);
editor.$blockScrolling = Infinity;
const renderer = editor.renderer;
renderer.setHighlightGutterLine(false);
renderer.setShowPrintMargin(false);
renderer.setOption('fontFamily', 'monospace');
renderer.setOption('fontSize', '14px');
renderer.setOption('minLines', '5');
renderer.setOption('maxLines', '15');
editor.setTheme('ace/theme/chrome');
Object.defineProperty(paragraph, 'ace', { value: editor });
};
};
/**
* Update caches list.
*/
const _refreshCaches = () => {
return agentMgr.publicCacheNames()
.then((cacheNames) => {
$scope.caches = _.sortBy(_.map(cacheNames, (name) => ({
label: maskCacheName(name, true),
value: name
})), (cache) => cache.label.toLowerCase());
_.forEach($scope.notebook.paragraphs, (paragraph) => {
if (!_.includes(cacheNames, paragraph.cacheName))
paragraph.cacheName = _.head(cacheNames);
});
// Await for demo caches.
if (!$ctrl.demoStarted && $root.IgniteDemoMode && nonEmpty(cacheNames)) {
$ctrl.demoStarted = true;
Loading.finish('sqlLoading');
_.forEach($scope.notebook.paragraphs, (paragraph) => $scope.execute(paragraph));
}
$scope.$applyAsync();
})
.catch((err) => Messages.showError(err));
};
const _startWatch = () => {
const finishLoading$ = defer(() => {
if (!$root.IgniteDemoMode)
Loading.finish('sqlLoading');
}).pipe(take(1));
const refreshCaches = (period) => {
return merge(timer(0, period).pipe(exhaustMap(() => _refreshCaches())), finishLoading$);
};
const cluster$ = agentMgr.connectionSbj.pipe(
pluck('cluster'),
distinctUntilChanged(),
tap((cluster) => {
this.clusterIsAvailable = (!!cluster && cluster.active === true) || agentMgr.isDemoMode();
})
);
this.refresh$ = cluster$.pipe(
switchMap((cluster) => {
if (!cluster && !agentMgr.isDemoMode()) {
return of(EMPTY).pipe(
tap(() => {
$scope.caches = [];
})
);
}
return of(cluster).pipe(
tap(() => Loading.start('sqlLoading')),
tap(() => {
_.forEach($scope.notebook.paragraphs, (paragraph) => {
paragraph.reset($interval);
});
}),
switchMap(() => refreshCaches(5000))
);
})
);
this.subscribers$ = merge(this.refresh$).subscribe();
};
const _newParagraph = (paragraph) => {
return new Paragraph($animate, $timeout, JavaTypes, errorParser, paragraph);
};
Notebook.find($state.params.noteId)
.then((notebook) => {
$scope.notebook = _.cloneDeep(notebook);
$scope.notebook_name = $scope.notebook.name;
if (!$scope.notebook.expandedParagraphs)
$scope.notebook.expandedParagraphs = [];
if (!$scope.notebook.paragraphs)
$scope.notebook.paragraphs = [];
$scope.notebook.paragraphs = _.map($scope.notebook.paragraphs, (p) => _newParagraph(p));
if (_.isEmpty($scope.notebook.paragraphs))
$scope.addQuery();
else
$scope.rebuildScrollParagraphs();
})
.then(() => {
if ($root.IgniteDemoMode && sessionStorage.showDemoInfo !== 'true') {
sessionStorage.showDemoInfo = 'true';
this.DemoInfo.show().then(_startWatch);
} else
_startWatch();
})
.catch(() => {
$scope.notebookLoadFailed = true;
Loading.finish('sqlLoading');
});
$scope.renameNotebook = (name) => {
if (!name)
return;
if ($scope.notebook.name !== name) {
const prevName = $scope.notebook.name;
$scope.notebook.name = name;
Notebook.save($scope.notebook)
.then(() => $scope.notebook.edit = false)
.catch((err) => {
$scope.notebook.name = prevName;
Messages.showError(err);
});
}
else
$scope.notebook.edit = false;
};
$scope.removeNotebook = (notebook) => Notebook.remove(notebook);
$scope.addParagraph = (paragraph, sz) => {
if ($scope.caches && $scope.caches.length > 0)
paragraph.cacheName = _.head($scope.caches).value;
$scope.notebook.paragraphs.push(paragraph);
$scope.notebook.expandedParagraphs.push(sz);
$scope.rebuildScrollParagraphs();
$location.hash(paragraph.id);
};
$scope.addQuery = function() {
const sz = $scope.notebook.paragraphs.length;
ActivitiesData.post({ group: 'sql', action: '/queries/add/query' });
const paragraph = _newParagraph({
name: 'Query' + (sz === 0 ? '' : sz),
query: '',
pageSize: $scope.pageSizesOptions[1].value,
timeLineSpan: $scope.timeLineSpans[0],
result: 'none',
rate: {
value: 1,
unit: 60000,
installed: false
},
qryType: 'query',
lazy: true
});
$scope.addParagraph(paragraph, sz);
$timeout(() => {
$anchorScroll();
paragraph.ace.focus();
});
};
$scope.addScan = function() {
const sz = $scope.notebook.paragraphs.length;
ActivitiesData.post({ group: 'sql', action: '/queries/add/scan' });
const paragraph = _newParagraph({
name: 'Scan' + (sz === 0 ? '' : sz),
query: '',
pageSize: $scope.pageSizesOptions[1].value,
timeLineSpan: $scope.timeLineSpans[0],
result: 'none',
rate: {
value: 1,
unit: 60000,
installed: false
},
qryType: 'scan'
});
$scope.addParagraph(paragraph, sz);
};
function _saveChartSettings(paragraph) {
if (!_.isEmpty(paragraph.charts)) {
const chart = paragraph.charts[0].api.getScope().chart;
if (!LegacyUtils.isDefined(paragraph.chartsOptions))
paragraph.chartsOptions = {barChart: {stacked: true}, areaChart: {style: 'stack'}};
switch (paragraph.result) {
case 'bar':
paragraph.chartsOptions.barChart.stacked = chart.stacked();
break;
case 'area':
paragraph.chartsOptions.areaChart.style = chart.style();
break;
default:
}
}
}
$scope.setResult = function(paragraph, new_result) {
if (paragraph.result === new_result)
return;
_saveChartSettings(paragraph);
paragraph.result = new_result;
if (paragraph.chart())
_chartApplySettings(paragraph, true);
};
$scope.resultEq = function(paragraph, result) {
return (paragraph.result === result);
};
$scope.paragraphExpanded = function(paragraph) {
const paragraph_idx = _.findIndex($scope.notebook.paragraphs, function(item) {
return paragraph === item;
});
const panel_idx = _.findIndex($scope.notebook.expandedParagraphs, function(item) {
return paragraph_idx === item;
});
return panel_idx >= 0;
};
const _columnFilter = function(paragraph) {
return paragraph.disabledSystemColumns || paragraph.systemColumns ? _allColumn : _hideColumn;
};
const _notObjectType = function(cls) {
return LegacyUtils.isJavaBuiltInClass(cls);
};
function _retainColumns(allCols, curCols, acceptableType, xAxis, unwantedCols) {
const retainedCols = [];
const availableCols = xAxis ? allCols : _.filter(allCols, function(col) {
return col.value >= 0;
});
if (availableCols.length > 0) {
curCols.forEach(function(curCol) {
const col = _.find(availableCols, {label: curCol.label});
if (col && acceptableType(col.type)) {
col.aggFx = curCol.aggFx;
retainedCols.push(col);
}
});
// If nothing was restored, add first acceptable column.
if (_.isEmpty(retainedCols)) {
let col;
if (unwantedCols)
col = _.find(availableCols, (avCol) => !_.find(unwantedCols, {label: avCol.label}) && acceptableType(avCol.type));
if (!col)
col = _.find(availableCols, (avCol) => acceptableType(avCol.type));
if (col)
retainedCols.push(col);
}
}
return retainedCols;
}
const _rebuildColumns = function(paragraph) {
_.forEach(_.groupBy(paragraph.meta, 'fieldName'), function(colsByName, fieldName) {
const colsByTypes = _.groupBy(colsByName, 'typeName');
const needType = _.keys(colsByTypes).length > 1;
_.forEach(colsByTypes, function(colsByType, typeName) {
_.forEach(colsByType, function(col, ix) {
col.fieldName = (needType && !LegacyUtils.isEmptyString(typeName) ? typeName + '.' : '') + fieldName + (ix > 0 ? ix : '');
});
});
});
paragraph.gridOptions.rebuildColumns();
paragraph.chartColumns = _.reduce(paragraph.meta, (acc, col, idx) => {
if (_notObjectType(col.fieldTypeName)) {
acc.push({
label: col.fieldName,
type: col.fieldTypeName,
aggFx: $scope.aggregateFxs[0],
value: idx.toString()
});
}
return acc;
}, []);
if (paragraph.chartColumns.length > 0) {
paragraph.chartColumns.push(TIME_LINE);
paragraph.chartColumns.push(ROW_IDX);
}
// We could accept onl not object columns for X axis.
paragraph.chartKeyCols = _retainColumns(paragraph.chartColumns, paragraph.chartKeyCols, _notObjectType, true);
// We could accept only numeric columns for Y axis.
paragraph.chartValCols = _retainColumns(paragraph.chartColumns, paragraph.chartValCols, _numberType, false, paragraph.chartKeyCols);
};
$scope.toggleSystemColumns = function(paragraph) {
if (paragraph.disabledSystemColumns)
return;
paragraph.columnFilter = _columnFilter(paragraph);
paragraph.chartColumns = [];
_rebuildColumns(paragraph);
};
/**
* Execute query and get first result page.
*
* @param qryType Query type. 'query' or `scan`.
* @param qryArg Argument with query properties.
* @param {(res) => any} onQueryStarted Action to execute when query ID is received.
* @return {Observable<VisorQueryResult>} Observable with first query result page.
*/
const _executeQuery0 = (qryType, qryArg, onQueryStarted: (res) => any = () => {}) => {
return from(qryType === 'scan' ? agentMgr.queryScan(qryArg) : agentMgr.querySql(qryArg)).pipe(
tap((res) => {
onQueryStarted(res);
$scope.$applyAsync();
}),
exhaustMap((res) => {
if (!_.isNil(res.rows))
return of(res);
const fetchFirstPageTask = timer(100, 500).pipe(
exhaustMap(() => agentMgr.queryFetchFistsPage(qryArg.nid, res.queryId, qryArg.pageSize)),
filter((res) => !_.isNil(res.rows))
);
const pingQueryTask = timer(60000, 60000).pipe(
exhaustMap(() => agentMgr.queryPing(qryArg.nid, res.queryId)),
takeWhile(({queryPingSupported}) => queryPingSupported),
ignoreElements()
);
return merge(fetchFirstPageTask, pingQueryTask);
}),
first()
);
};
/**
* Execute query with old query clearing and showing of query result.
*
* @param paragraph Query paragraph.
* @param qryArg Argument with query properties.
* @param {(res) => any} onQueryStarted Action to execute when query ID is received.
* @param {(res) => any} onQueryFinished Action to execute when first query result page is received.
* @param {(err) => any} onError Action to execute when error occured.
* @return {Observable<VisorQueryResult>} Observable with first query result page.
*/
const _executeQuery = (
paragraph,
qryArg,
onQueryStarted: (res) => any = () => {},
onQueryFinished: (res) => any = () => {},
onError: (err) => any = () => {}
) => {
return from(_closeOldQuery(paragraph)).pipe(
switchMap(() => _executeQuery0(paragraph.qryType, qryArg, onQueryStarted)),
tap((res) => {
onQueryFinished(res);
$scope.$applyAsync();
}),
takeUntil(paragraph.cancelQuerySubject),
catchError((err) => {
onError(err);
$scope.$applyAsync();
return of(err);
})
);
};
/**
* Execute query and get all query results.
*
* @param paragraph Query paragraph.
* @param qryArg Argument with query properties.
* @param {(res) => any} onQueryStarted Action to execute when query ID is received.
* @param {(res) => any} onQueryFinished Action to execute when first query result page is received.
* @param {(err) => any} onError Action to execute when error occured.
* @return {Observable<any>} Observable with full query result.
*/
const _exportQueryAll = (
paragraph,
qryArg,
onQueryStarted: (res) => any = () => {},
onQueryFinished: (res) => any = () => {},
onError: (err) => any = () => {}
) => {
return from(_closeOldExport(paragraph)).pipe(
switchMap(() => _executeQuery0(paragraph.qryType, qryArg, onQueryStarted)),
expand((acc) => {
return from(agentMgr.queryNextPage(acc.responseNodeId, acc.queryId, qryArg.pageSize)
.then((res) => {
acc.rows = acc.rows.concat(res.rows);
acc.hasMore = res.hasMore;
return acc;
}));
}),
first((acc) => !acc.hasMore),
tap(onQueryFinished),
takeUntil(paragraph.cancelExportSubject),
catchError((err) => {
onError(err);
return of(err);
})
);
};
/**
* @param {Object} paragraph Query
* @param {Boolean} clearChart Flag is need clear chart model.
* @param {{columns: Array, rows: Array, responseNodeId: String, queryId: int, hasMore: Boolean}} res Query results.
* @private
*/
const _processQueryResult = (paragraph, clearChart, res) => {
const prevKeyCols = paragraph.chartKeyCols;
const prevValCols = paragraph.chartValCols;
if (!_.eq(paragraph.meta, res.columns)) {
paragraph.meta = [];
paragraph.chartColumns = [];
if (!LegacyUtils.isDefined(paragraph.chartKeyCols))
paragraph.chartKeyCols = [];
if (!LegacyUtils.isDefined(paragraph.chartValCols))
paragraph.chartValCols = [];
if (res.columns.length) {
const _key = _.find(res.columns, {fieldName: '_KEY'});
const _val = _.find(res.columns, {fieldName: '_VAL'});
paragraph.disabledSystemColumns = !(_key && _val) ||
(res.columns.length === 2 && _key && _val) ||
(res.columns.length === 1 && (_key || _val));
}
paragraph.columnFilter = _columnFilter(paragraph);
paragraph.meta = res.columns;
_rebuildColumns(paragraph);
}
paragraph.page = 1;
paragraph.total = 0;
paragraph.duration = res.duration;
paragraph.queryId = res.hasMore ? res.queryId : null;
paragraph.resNodeId = res.responseNodeId;
paragraph.setError({message: ''});
// Prepare explain results for display in table.
if (paragraph.queryArgs.query && paragraph.queryArgs.query.startsWith('EXPLAIN') && res.rows) {
paragraph.rows = [];
res.rows.forEach((row, i) => {
const line = res.rows.length - 1 === i ? row[0] : row[0] + '\n';
line.replace(/\"/g, '').split('\n').forEach((ln) => paragraph.rows.push([ln]));
});
}
else
paragraph.rows = res.rows;
paragraph.gridOptions.adjustHeight(paragraph.rows.length);
const chartHistory = paragraph.chartHistory;
// Clear history on query change.
if (clearChart) {
chartHistory.length = 0;
_.forEach(paragraph.charts, (chart) => chart.data.length = 0);
}
// Add results to history.
chartHistory.push({tm: new Date(), rows: paragraph.rows});
// Keep history size no more than max length.
while (chartHistory.length > HISTORY_LENGTH)
chartHistory.shift();
paragraph.showLoading(false);
if (_.isNil(paragraph.result) || paragraph.result === 'none' || paragraph.scanExplain())
paragraph.result = 'table';
else if (paragraph.chart()) {
let resetCharts = clearChart;
if (!resetCharts) {
const curKeyCols = paragraph.chartKeyCols;
const curValCols = paragraph.chartValCols;
resetCharts = !prevKeyCols || !prevValCols ||
prevKeyCols.length !== curKeyCols.length ||
prevValCols.length !== curValCols.length;
}
_chartApplySettings(paragraph, resetCharts);
}
};
const _fetchQueryResult = (paragraph, clearChart, res) => {
_processQueryResult(paragraph, clearChart, res);
_tryStartRefresh(paragraph);
};
const _closeOldQuery = (paragraph) => {
const nid = paragraph.resNodeId;
if (paragraph.queryId) {
const qryId = paragraph.queryId;
delete paragraph.queryId;
return agentMgr.queryClose(nid, qryId);
}
return $q.when();
};
const _closeOldExport = (paragraph) => {
const nid = paragraph.exportNodeId;
if (paragraph.exportId) {
const exportId = paragraph.exportId;
delete paragraph.exportId;
return agentMgr.queryClose(nid, exportId);
}
return $q.when();
};
$scope.cancelQuery = (paragraph) => {
paragraph.cancelQuerySubject.next(true);
this.$scope.stopRefresh(paragraph);
_closeOldQuery(paragraph)
.catch((err) => paragraph.setError(err))
.finally(() => paragraph.showLoading(false));
};
/**
* @param {String} name Cache name.
* @param {Array.<String>} nids Cache name.
* @return {Promise<Array.<{nid: string, ip: string, version:string, gridName: string, os: string, client: boolean}>>}
*/
const cacheNodesModel = (name, nids) => {
return agentMgr.topology(true)
.then((nodes) =>
_.reduce(nodes, (acc, node) => {
if (_.includes(nids, node.nodeId)) {
acc.push({
nid: node.nodeId.toUpperCase(),
ip: _.head(node.attributes['org.apache.ignite.ips'].split(', ')),
version: node.attributes['org.apache.ignite.build.ver'],
gridName: node.attributes['org.apache.ignite.ignite.name'],
os: `${node.attributes['os.name']} ${node.attributes['os.arch']} ${node.attributes['os.version']}`,
client: node.attributes['org.apache.ignite.cache.client']
});
}
return acc;
}, [])
);
};
/**
* @param {string} name Cache name.
* @param {boolean} local Local query.
* @return {Promise<string>} Nid
*/
const _chooseNode = (name, local) => {
if (_.isEmpty(name))
return Promise.resolve(null);
return agentMgr.cacheNodes(name)
.then((nids) => {
if (local) {
return cacheNodesModel(name, nids)
.then((nodes) => Nodes.selectNode(nodes, name).catch(() => {}))
.then((selectedNids) => _.head(selectedNids));
}
return nids[_.random(0, nids.length - 1)];
})
.catch(Messages.showError);
};
const _executeRefresh = (paragraph) => {
const args = paragraph.queryArgs;
from(agentMgr.awaitCluster()).pipe(
switchMap(() => args.localNid ? of(args.localNid) : from(_chooseNode(args.cacheName, false))),
switchMap((nid) => {
paragraph.showLoading(true);
const qryArg = {
nid,
cacheName: args.cacheName,
query: args.query,
nonCollocatedJoins: args.nonCollocatedJoins,
enforceJoinOrder: args.enforceJoinOrder,
replicatedOnly: false,
local: !!args.localNid,
pageSize: args.pageSize,
lazy: args.lazy,
collocated: args.collocated
};
return _executeQuery(
paragraph,
qryArg,
(res) => _initQueryResult(paragraph, res),
(res) => _fetchQueryResult(paragraph, false, res),
(err) => {
paragraph.setError(err);
paragraph.ace && paragraph.ace.focus();
$scope.stopRefresh(paragraph);
}
);
}),
finalize(() => paragraph.showLoading(false))
).toPromise();
};
const _tryStartRefresh = function(paragraph) {
if (_.get(paragraph, 'rate.installed') && paragraph.queryExecuted() && paragraph.nonRefresh()) {
$scope.chartAcceptKeyColumn(paragraph, TIME_LINE);
const delay = paragraph.rate.value * paragraph.rate.unit;
paragraph.rate.stopTime = $interval(_executeRefresh, delay, 0, false, paragraph);
}
};
const addLimit = (query, limitSize) =>
`SELECT * FROM (
${query}
) LIMIT ${limitSize}`;
$scope.nonCollocatedJoinsAvailable = () => {
return Version.since(this.agentMgr.clusterVersion, NON_COLLOCATED_JOINS_SINCE);
};
$scope.collocatedJoinsAvailable = () => {
return Version.since(this.agentMgr.clusterVersion, ...COLLOCATED_QUERY_SINCE);
};
$scope.enforceJoinOrderAvailable = () => {
return Version.since(this.agentMgr.clusterVersion, ...ENFORCE_JOIN_SINCE);
};
$scope.lazyQueryAvailable = () => {
return Version.since(this.agentMgr.clusterVersion, ...LAZY_QUERY_SINCE);
};
$scope.ddlAvailable = () => {
return Version.since(this.agentMgr.clusterVersion, ...DDL_SINCE);
};
$scope.cacheNameForSql = (paragraph) => {
return $scope.ddlAvailable() && !paragraph.useAsDefaultSchema ? null : paragraph.cacheName;
};
const _initQueryResult = (paragraph, res) => {
paragraph.resNodeId = res.responseNodeId;
paragraph.queryId = res.queryId;
if (paragraph.nonRefresh()) {
paragraph.rows = [];
paragraph.meta = [];
paragraph.setError({message: ''});
}
paragraph.hasNext = false;
};
const _initExportResult = (paragraph, res) => {
paragraph.exportNodeId = res.responseNodeId;
paragraph.exportId = res.queryId;
};
$scope.execute = (paragraph, local = false) => {
if (!$scope.queryAvailable(paragraph))
return;
const nonCollocatedJoins = !!paragraph.nonCollocatedJoins;
const enforceJoinOrder = !!paragraph.enforceJoinOrder;
const lazy = !!paragraph.lazy;
const collocated = !!paragraph.collocated;
_cancelRefresh(paragraph);
from(_chooseNode(paragraph.cacheName, local)).pipe(
switchMap((nid) => {
// If we are executing only selected part of query then Notebook shouldn't be saved.
if (!paragraph.partialQuery)
Notebook.save($scope.notebook).catch(Messages.showError);
paragraph.localQueryMode = local;
paragraph.prevQuery = paragraph.queryArgs ? paragraph.queryArgs.query : paragraph.query;
paragraph.showLoading(true);
const query = paragraph.partialQuery || paragraph.query;
const args = paragraph.queryArgs = {
cacheName: $scope.cacheNameForSql(paragraph),
query,
pageSize: paragraph.pageSize,
maxPages: paragraph.maxPages,
nonCollocatedJoins,
enforceJoinOrder,
localNid: local ? nid : null,
lazy,
collocated
};
ActivitiesData.post({ group: 'sql', action: '/queries/execute' });
const qry = args.maxPages ? addLimit(args.query, args.pageSize * args.maxPages) : query;
const qryArg = {
nid,
cacheName: args.cacheName,
query: qry,
nonCollocatedJoins,
enforceJoinOrder,
replicatedOnly: false,
local,
pageSize: args.pageSize,
lazy,
collocated
};
return _executeQuery(
paragraph,
qryArg,
(res) => _initQueryResult(paragraph, res),
(res) => _fetchQueryResult(paragraph, true, res),
(err) => {
paragraph.setError(err);
paragraph.ace && paragraph.ace.focus();
$scope.stopRefresh(paragraph);
Messages.showError(err);
}
);
}),
finalize(() => paragraph.showLoading(false))
).toPromise();
};
const _cancelRefresh = (paragraph) => {
if (paragraph.rate && paragraph.rate.stopTime) {
delete paragraph.queryArgs;
_.set(paragraph, 'rate.installed', false);
$interval.cancel(paragraph.rate.stopTime);
delete paragraph.rate.stopTime;
}
};
$scope.explain = (paragraph) => {
if (!$scope.queryAvailable(paragraph))
return;
const nonCollocatedJoins = !!paragraph.nonCollocatedJoins;
const enforceJoinOrder = !!paragraph.enforceJoinOrder;
const collocated = !!paragraph.collocated;
if (!paragraph.partialQuery)
Notebook.save($scope.notebook).catch(Messages.showError);
_cancelRefresh(paragraph);
paragraph.showLoading(true);
from(_chooseNode(paragraph.cacheName, false)).pipe(
switchMap((nid) => {
const qryArg = paragraph.queryArgs = {
nid,
cacheName: $scope.cacheNameForSql(paragraph),
query: 'EXPLAIN ' + (paragraph.partialQuery || paragraph.query),
nonCollocatedJoins,
enforceJoinOrder,
replicatedOnly: false,
local: false,
pageSize: paragraph.pageSize,
lazy: false,
collocated
};
ActivitiesData.post({ group: 'sql', action: '/queries/explain' });
return _executeQuery(
paragraph,
qryArg,
(res) => _initQueryResult(paragraph, res),
(res) => _fetchQueryResult(paragraph, true, res),
(err) => {
paragraph.setError(err);
paragraph.ace && paragraph.ace.focus();
}
);
}),
finalize(() => paragraph.showLoading(false))
).toPromise();
};
$scope.scan = (paragraph, local = false) => {
if (!$scope.scanAvailable(paragraph))
return;
const cacheName = paragraph.cacheName;
const caseSensitive = !!paragraph.caseSensitive;
const filter = paragraph.filter;
const pageSize = paragraph.pageSize;
from(_chooseNode(cacheName, local)).pipe(
switchMap((nid) => {
paragraph.localQueryMode = local;
Notebook.save($scope.notebook)
.catch(Messages.showError);
paragraph.showLoading(true);
const qryArg = paragraph.queryArgs = {
cacheName,
filter,
regEx: false,
caseSensitive,
near: false,
pageSize,
localNid: local ? nid : null
};
qryArg.nid = nid;
qryArg.local = local;
ActivitiesData.post({ group: 'sql', action: '/queries/scan' });
return _executeQuery(
paragraph,
qryArg,
(res) => _initQueryResult(paragraph, res),
(res) => _fetchQueryResult(paragraph, true, res),
(err) => paragraph.setError(err)
);
}),
finalize(() => paragraph.showLoading(false))
).toPromise();
};
function _updatePieChartsWithData(paragraph, newDatum) {
$timeout(() => {
_.forEach(paragraph.charts, function(chart) {
const chartDatum = chart.data;
chartDatum.length = 0;
_.forEach(newDatum, function(series) {
if (chart.options.title.text === series.key)
_.forEach(series.values, (v) => chartDatum.push(v));
});
});
_.forEach(paragraph.charts, (chart) => chart.api.update());
});
}
const _processQueryNextPage = (paragraph, res) => {
paragraph.page++;
paragraph.total += paragraph.rows.length;
paragraph.duration = res.duration;
paragraph.rows = res.rows;
if (paragraph.chart()) {
if (paragraph.result === 'pie')
_updatePieChartsWithData(paragraph, _pieChartDatum(paragraph));
else
_updateChartsWithData(paragraph, _chartDatum(paragraph));
}
paragraph.gridOptions.adjustHeight(paragraph.rows.length);
paragraph.showLoading(false);
if (!res.hasMore)
delete paragraph.queryId;
};
$scope.nextPage = (paragraph) => {
paragraph.showLoading(true);
paragraph.queryArgs.pageSize = paragraph.pageSize;
const nextPageTask = from(agentMgr.queryNextPage(paragraph.resNodeId, paragraph.queryId, paragraph.pageSize)
.then((res) => _processQueryNextPage(paragraph, res))
.catch((err) => {
paragraph.setError(err);
paragraph.ace && paragraph.ace.focus();
}));
const pingQueryTask = timer(60000, 60000).pipe(
exhaustMap(() => agentMgr.queryPing(paragraph.resNodeId, paragraph.queryId)),
takeWhile(({queryPingSupported}) => queryPingSupported),
ignoreElements()
);
merge(nextPageTask, pingQueryTask).pipe(
take(1),
takeUntil(paragraph.cancelQuerySubject)
).subscribe();
};
const _export = (fileName, columnDefs, meta, rows, toClipBoard = false) => {
const csvSeparator = this.CSV.getSeparator();
let csvContent = '';
const cols = [];
const excludedCols = [];
_.forEach(meta, (col, idx) => {
if (columnDefs[idx].visible)
cols.push(_fullColName(col));
else
excludedCols.push(idx);
});
csvContent += cols.join(csvSeparator) + '\n';
_.forEach(rows, (row) => {
cols.length = 0;
if (Array.isArray(row)) {
_.forEach(row, (elem, idx) => {
if (_.includes(excludedCols, idx))
return;
cols.push(_.isUndefined(elem) ? '' : JSON.stringify(elem));
});
}
else {
_.forEach(columnDefs, (col) => {
if (col.visible) {
const elem = row[col.fieldName];
cols.push(_.isUndefined(elem) ? '' : JSON.stringify(elem));
}
});
}
csvContent += cols.join(csvSeparator) + '\n';
});
if (toClipBoard)
IgniteCopyToClipboard.copy(csvContent);
else
LegacyUtils.download('text/csv', fileName, csvContent);
};
/**
* Generate file name with query results.
*
* @param paragraph {Object} Query paragraph .
* @param all {Boolean} All result export flag.
* @returns {string}
*/
const exportFileName = (paragraph, all) => {
const args = paragraph.queryArgs;
if (paragraph.qryType === 'scan')
return `export-scan-${args.cacheName}-${paragraph.name}${all ? '-all' : ''}.csv`;
return `export-query-${paragraph.name}${all ? '-all' : ''}.csv`;
};
$scope.exportCsvToClipBoard = (paragraph) => {
_export(exportFileName(paragraph, false), paragraph.gridOptions.columnDefs, paragraph.meta, paragraph.rows, true);
};
$scope.exportCsv = function(paragraph) {
_export(exportFileName(paragraph, false), paragraph.gridOptions.columnDefs, paragraph.meta, paragraph.rows);
// paragraph.gridOptions.api.exporter.csvExport(uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE);
};
$scope.exportPdf = function(paragraph) {
paragraph.gridOptions.api.exporter.pdfExport(uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE);
};
$scope.exportCsvAll = (paragraph) => {
const args = paragraph.queryArgs;
paragraph.cancelExportSubject.next(true);
paragraph.csvIsPreparing = true;
return (args.localNid ? of(args.localNid) : from(_chooseNode(args.cacheName, false))).pipe(
map((nid) => _.assign({}, args, {nid, pageSize: 1024, local: !!args.localNid, replicatedOnly: false})),
switchMap((arg) => _exportQueryAll(
paragraph,
arg,
(res) => _initExportResult(paragraph, res),
(res) => _export(exportFileName(paragraph, true), paragraph.gridOptions.columnDefs, res.columns, res.rows),
(err) => {
Messages.showError(err);
return of(err);
}
)),
finalize(() => paragraph.csvIsPreparing = false)
).toPromise();
};
// $scope.exportPdfAll = function(paragraph) {
// $http.post('/api/v1/agent/query/getAll', {query: paragraph.query, cacheName: paragraph.cacheName})
// .then(({data}) {
// _export(paragraph.name + '-all.csv', data.meta, data.rows);
// })
// .catch(Messages.showError);
// };
$scope.rateAsString = function(paragraph) {
if (paragraph.rate && paragraph.rate.installed) {
const idx = _.findIndex($scope.timeUnit, function(unit) {
return unit.value === paragraph.rate.unit;
});
if (idx >= 0)
return ' ' + paragraph.rate.value + $scope.timeUnit[idx].short;
paragraph.rate.installed = false;
}
return '';
};
$scope.startRefresh = function(paragraph, value, unit) {
$scope.stopRefresh(paragraph);
paragraph.rate.value = value;
paragraph.rate.unit = unit;
paragraph.rate.installed = true;
if (paragraph.queryExecuted() && !paragraph.scanExplain())
_executeRefresh(paragraph);
};
$scope.stopRefresh = function(paragraph) {
_.set(paragraph, 'rate.installed', false);
_tryStopRefresh(paragraph);
};
$scope.paragraphTimeSpanVisible = function(paragraph) {
return paragraph.timeLineSupported() && paragraph.chartTimeLineEnabled();
};
$scope.paragraphTimeLineSpan = function(paragraph) {
if (paragraph && paragraph.timeLineSpan)
return paragraph.timeLineSpan.toString();
return '1';
};
$scope.applyChartSettings = function(paragraph) {
_chartApplySettings(paragraph, true);
};
$scope.queryAvailable = function(paragraph) {
return paragraph.query && !paragraph.loading;
};
$scope.queryTooltip = function(paragraph, action) {
if ($scope.queryAvailable(paragraph))
return;
if (paragraph.loading)
return 'Waiting for server response';
return 'Input text to ' + action;
};
$scope.scanAvailable = function(paragraph) {
return $scope.caches.length && !(paragraph.loading || paragraph.csvIsPreparing);
};
$scope.scanTooltip = function(paragraph) {
if ($scope.scanAvailable(paragraph))
return;
if (paragraph.loading)
return 'Waiting for server response';
return 'Select cache to export scan results';
};
$scope.clickableMetadata = function(node) {
return node.type.slice(0, 5) !== 'index';
};
$scope.dblclickMetadata = function(paragraph, node) {
paragraph.ace.insert(node.name);
setTimeout(() => paragraph.ace.focus(), 100);
};
$scope.importMetadata = function() {
Loading.start('loadingCacheMetadata');
$scope.metadata = [];
agentMgr.metadata()
.then((metadata) => {
$scope.metadata = _.sortBy(_.filter(metadata, (meta) => {
const cache = _.find($scope.caches, { value: meta.cacheName });
if (cache) {
meta.name = (cache.sqlSchema || '"' + meta.cacheName + '"') + '.' + meta.typeName;
meta.displayName = (cache.sqlSchema || meta.maskedName) + '.' + meta.typeName;
if (cache.sqlSchema)
meta.children.unshift({type: 'plain', name: 'cacheName: ' + meta.maskedName, maskedName: meta.maskedName});
meta.children.unshift({type: 'plain', name: 'mode: ' + cache.mode, maskedName: meta.maskedName});
}
return cache;
}), 'name');
})
.catch(Messages.showError)
.then(() => Loading.finish('loadingCacheMetadata'));
};
$scope.showResultQuery = function(paragraph) {
if (!_.isNil(paragraph)) {
const scope = $scope.$new();
if (paragraph.qryType === 'scan') {
scope.title = 'SCAN query';
const filter = paragraph.queryArgs.filter;
if (_.isEmpty(filter))
scope.content = [`SCAN query for cache: <b>${maskCacheName(paragraph.queryArgs.cacheName, true)}</b>`];
else
scope.content = [`SCAN query for cache: <b>${maskCacheName(paragraph.queryArgs.cacheName, true)}</b> with filter: <b>${filter}</b>`];
}
else if (paragraph.queryArgs.query.startsWith('EXPLAIN ')) {
scope.title = 'Explain query';
scope.content = paragraph.queryArgs.query.split(/\r?\n/);
}
else {
scope.title = 'SQL query';
scope.content = paragraph.queryArgs.query.split(/\r?\n/);
}
// Attach duration and selected node info
scope.meta = `Duration: ${$filter('duration')(paragraph.duration)}.`;
scope.meta += paragraph.localQueryMode ? ` Node ID8: ${id8(paragraph.resNodeId)}` : '';
// Show a basic modal from a controller
$modal({scope, templateUrl: messageTemplateUrl, show: true});
}
};
$scope.showStackTrace = function(paragraph) {
if (!_.isNil(paragraph)) {
const scope = $scope.$new();
scope.title = 'Error details';
scope.content = [];
const tab = '&nbsp;&nbsp;&nbsp;&nbsp;';
const addToTrace = (item) => {
if (nonNil(item)) {
scope.content.push((scope.content.length > 0 ? tab : '') + errorParser.extractFullMessage(item));
addToTrace(item.cause);
_.forEach(item.suppressed, (sup) => addToTrace(sup));
}
};
addToTrace(paragraph.error.root);
// Show a basic modal from a controller
$modal({scope, templateUrl: messageTemplateUrl, show: true});
}
};
this.offTransitions = $transitions.onBefore({from: 'base.sql.notebook'}, ($transition$) => {
const options = $transition$.options();
// Skip query closing in case of auto redirection on state change.
if (options.redirectedFrom)
return true;
return this.closeOpenedQueries();
});
$window.addEventListener('beforeunload', this.closeOpenedQueries);
this.onClusterSwitchLnr = () => {
const paragraphs = _.get(this, '$scope.notebook.paragraphs');
if (this._hasRunningQueries(paragraphs)) {
try {
return Confirm.confirm('You have running queries. Are you sure you want to cancel them?')
.then(() => this._closeOpenedQueries(paragraphs));
}
catch (err) {
return Promise.reject(new CancellationError());
}
}
return Promise.resolve(true);
};
agentMgr.addClusterSwitchListener(this.onClusterSwitchLnr);
}
_closeOpenedQueries(paragraphs) {
return Promise.all(_.map(paragraphs, (paragraph) => {
paragraph.cancelQuerySubject.next(true);
paragraph.cancelExportSubject.next(true);
return Promise.all([paragraph.queryId
? this.agentMgr.queryClose(paragraph.resNodeId, paragraph.queryId)
.catch(() => Promise.resolve(true))
.finally(() => delete paragraph.queryId)
: Promise.resolve(true),
paragraph.csvIsPreparing && paragraph.exportId
? this.agentMgr.queryClose(paragraph.exportNodeId, paragraph.exportId)
.catch(() => Promise.resolve(true))
.finally(() => delete paragraph.exportId)
: Promise.resolve(true)]
);
}));
}
_hasRunningQueries(paragraphs) {
return !!_.find(paragraphs,
(paragraph) => paragraph.loading || paragraph.scanningInProgress || paragraph.csvIsPreparing);
}
async closeOpenedQueries() {
const paragraphs = _.get(this, '$scope.notebook.paragraphs');
if (this._hasRunningQueries(paragraphs)) {
try {
await this.Confirm.confirm('You have running queries. Are you sure you want to cancel them?');
this._closeOpenedQueries(paragraphs);
return true;
}
catch (ignored) {
return false;
}
}
return true;
}
scanActions: QueryActions<Paragraph & {type: 'scan'}> = [
{
text: 'Scan',
click: (p) => this.$scope.scan(p),
available: (p) => this.$scope.scanAvailable(p)
},
{
text: 'Scan on selected node',
click: (p) => this.$scope.scan(p, true),
available: (p) => this.$scope.scanAvailable(p)
},
{text: 'Rename', click: (p) => this.renameParagraph(p), available: () => true},
{text: 'Remove', click: (p) => this.removeParagraph(p), available: () => true}
];
queryActions: QueryActions<Paragraph & {type: 'query'}> = [
{
text: 'Execute',
click: (p) => this.$scope.execute(p),
available: (p) => this.$scope.queryAvailable(p)
},
{
text: 'Execute on selected node',
click: (p) => this.$scope.execute(p, true),
available: (p) => this.$scope.queryAvailable(p)
},
{
text: 'Explain',
click: (p) => this.$scope.explain(p),
available: (p) => this.$scope.queryAvailable(p)
},
{text: 'Rename', click: (p) => this.renameParagraph(p), available: () => true},
{text: 'Remove', click: (p) => this.removeParagraph(p), available: () => true}
];
async renameParagraph(paragraph: Paragraph) {
try {
const newName = await this.IgniteInput.input('Rename Query', 'New query name:', paragraph.name);
if (paragraph.name !== newName) {
paragraph.name = newName;
this.$scope.rebuildScrollParagraphs();
await this.Notebook.save(this.$scope.notebook)
.catch(this.Messages.showError);
}
}
catch (ignored) {
// No-op.
}
}
async removeParagraph(paragraph: Paragraph) {
try {
const msg = (this._hasRunningQueries([paragraph])
? 'Query is being executed. Are you sure you want to cancel and remove query: "'
: 'Are you sure you want to remove query: "') + paragraph.name + '"?';
await this.Confirm.confirm(msg);
this.$scope.stopRefresh(paragraph);
this._closeOpenedQueries([paragraph]);
const paragraph_idx = _.findIndex(this.$scope.notebook.paragraphs, (item) => paragraph === item);
const panel_idx = _.findIndex(this.$scope.expandedParagraphs, (item) => paragraph_idx === item);
if (panel_idx >= 0)
this.$scope.expandedParagraphs.splice(panel_idx, 1);
this.$scope.notebook.paragraphs.splice(paragraph_idx, 1);
this.$scope.rebuildScrollParagraphs();
paragraph.cancelQuerySubject.complete();
paragraph.cancelExportSubject.complete();
await this.Notebook.save(this.$scope.notebook)
.catch(this.Messages.showError);
}
catch (ignored) {
// No-op.
}
}
isParagraphOpened(index: number) {
return this.$scope.notebook.expandedParagraphs.includes(index);
}
onParagraphClose(index: number) {
const expanded = this.$scope.notebook.expandedParagraphs;
expanded.splice(expanded.indexOf(index), 1);
}
onParagraphOpen(index: number) {
this.$scope.notebook.expandedParagraphs.push(index);
}
$onDestroy() {
if (this.subscribers$)
this.subscribers$.unsubscribe();
if (this.offTransitions)
this.offTransitions();
this.agentMgr.removeClusterSwitchListener(this.onClusterSwitchLnr);
this.$window.removeEventListener('beforeunload', this.closeOpenedQueries);
}
}