| /* |
| * 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 {timer, merge, defer, of, EMPTY} from 'rxjs'; |
| import {tap, switchMap, exhaustMap, take, pluck, distinctUntilChanged, filter, map, catchError} 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'; |
| |
| // Time line X axis descriptor. |
| const TIME_LINE = {value: -1, type: 'java.sql.Date', label: 'TIME_LINE'}; |
| |
| // Row index X axis descriptor. |
| const ROW_IDX = {value: -2, type: 'java.lang.Integer', label: 'ROW_IDX'}; |
| |
| const NON_COLLOCATED_JOINS_SINCE = '1.7.0'; |
| |
| const COLLOCATED_QUERY_SINCE = [['2.3.5', '2.4.0'], ['2.4.6', '2.5.0'], ['2.5.1-p13', '2.6.0'], '2.7.0']; |
| |
| const ENFORCE_JOIN_SINCE = [['1.7.9', '1.8.0'], ['1.8.4', '1.9.0'], '1.9.1']; |
| |
| const LAZY_QUERY_SINCE = [['2.1.4-p1', '2.2.0'], '2.2.1']; |
| |
| const DDL_SINCE = [['2.1.6', '2.2.0'], '2.3.0']; |
| |
| const _fullColName = (col) => { |
| const res = []; |
| |
| if (col.schemaName) |
| res.push(col.schemaName); |
| |
| if (col.typeName) |
| res.push(col.typeName); |
| |
| res.push(col.fieldName); |
| |
| return res.join('.'); |
| }; |
| |
| let paragraphId = 0; |
| |
| class Paragraph { |
| name: string; |
| qryType: 'scan' | 'query'; |
| |
| constructor($animate, $timeout, JavaTypes, errorParser, paragraph) { |
| const self = this; |
| |
| self.id = 'paragraph-' + paragraphId++; |
| self.qryType = paragraph.qryType || 'query'; |
| self.maxPages = 0; |
| self.filter = ''; |
| self.useAsDefaultSchema = false; |
| self.localQueryMode = false; |
| self.csvIsPreparing = false; |
| self.scanningInProgress = false; |
| |
| _.assign(this, paragraph); |
| |
| Object.defineProperty(this, 'gridOptions', {value: { |
| enableGridMenu: false, |
| enableColumnMenus: false, |
| flatEntityAccess: true, |
| fastWatch: true, |
| categories: [], |
| rebuildColumns() { |
| if (_.isNil(this.api)) |
| return; |
| |
| this.categories.length = 0; |
| |
| this.columnDefs = _.reduce(self.meta, (cols, col, idx) => { |
| cols.push({ |
| displayName: col.fieldName, |
| headerTooltip: _fullColName(col), |
| field: idx.toString(), |
| minWidth: 50, |
| cellClass: 'cell-left', |
| visible: self.columnFilter(col) |
| }); |
| |
| this.categories.push({ |
| name: col.fieldName, |
| visible: self.columnFilter(col), |
| enableHiding: true |
| }); |
| |
| return cols; |
| }, []); |
| |
| $timeout(() => this.api.core.notifyDataChange('column')); |
| }, |
| adjustHeight() { |
| if (_.isNil(this.api)) |
| return; |
| |
| this.data = self.rows; |
| |
| const height = Math.min(self.rows.length, 15) * 30 + 47; |
| |
| // Remove header height. |
| this.api.grid.element.css('height', height + 'px'); |
| |
| $timeout(() => this.api.core.handleWindowResize()); |
| }, |
| onRegisterApi(api) { |
| $animate.enabled(api.grid.element, false); |
| |
| this.api = api; |
| |
| this.rebuildColumns(); |
| |
| this.adjustHeight(); |
| } |
| }}); |
| |
| Object.defineProperty(this, 'chartHistory', {value: []}); |
| |
| Object.defineProperty(this, 'error', {value: { |
| root: {}, |
| message: '' |
| }}); |
| |
| this.setError = (err) => { |
| this.error.root = err; |
| this.error.message = errorParser.extractMessage(err); |
| |
| let cause = err; |
| |
| while (nonNil(cause)) { |
| if (nonEmpty(cause.className) && |
| _.includes(['SQLException', 'JdbcSQLException', 'QueryCancelledException'], JavaTypes.shortClassName(cause.className))) { |
| this.error.message = errorParser.extractMessage(cause.message || cause.className); |
| |
| break; |
| } |
| |
| cause = cause.cause; |
| } |
| |
| if (_.isEmpty(this.error.message) && nonEmpty(err.className)) { |
| this.error.message = 'Internal cluster error'; |
| |
| if (nonEmpty(err.className)) |
| this.error.message += ': ' + err.className; |
| } |
| }; |
| } |
| |
| resultType() { |
| if (_.isNil(this.queryArgs)) |
| return null; |
| |
| if (nonEmpty(this.error.message)) |
| return 'error'; |
| |
| if (_.isEmpty(this.rows)) |
| return 'empty'; |
| |
| return this.result === 'table' ? 'table' : 'chart'; |
| } |
| |
| nonRefresh() { |
| return _.isNil(this.rate) || _.isNil(this.rate.stopTime); |
| } |
| |
| table() { |
| return this.result === 'table'; |
| } |
| |
| chart() { |
| return this.result !== 'table' && this.result !== 'none'; |
| } |
| |
| nonEmpty() { |
| return this.rows && this.rows.length > 0; |
| } |
| |
| queryExecuted() { |
| return nonEmpty(this.meta) || nonEmpty(this.error.message); |
| } |
| |
| scanExplain() { |
| return this.queryExecuted() && this.queryArgs.type !== 'QUERY'; |
| } |
| |
| timeLineSupported() { |
| return this.result !== 'pie'; |
| } |
| |
| chartColumnsConfigured() { |
| return nonEmpty(this.chartKeyCols) && nonEmpty(this.chartValCols); |
| } |
| |
| chartTimeLineEnabled() { |
| return nonEmpty(this.chartKeyCols) && _.eq(this.chartKeyCols[0], TIME_LINE); |
| } |
| |
| executionInProgress(showLocal = false) { |
| return this.loading && (this.localQueryMode === showLocal); |
| } |
| |
| checkScanInProgress(showLocal = false) { |
| return this.scanningInProgress && (this.localQueryMode === showLocal); |
| } |
| |
| cancelRefresh($interval) { |
| if (this.rate && this.rate.stopTime) { |
| $interval.cancel(this.rate.stopTime); |
| |
| delete this.rate.stopTime; |
| } |
| } |
| |
| reset($interval) { |
| this.meta = []; |
| this.chartColumns = []; |
| this.chartKeyCols = []; |
| this.chartValCols = []; |
| this.error.root = {}; |
| this.error.message = ''; |
| this.rows = []; |
| this.duration = 0; |
| |
| this.cancelRefresh($interval); |
| } |
| } |
| |
| // Controller for SQL notebook screen. |
| export class NotebookCtrl { |
| static $inject = ['IgniteInput', '$rootScope', '$scope', '$http', '$q', '$timeout', '$interval', '$animate', '$location', '$anchorScroll', '$state', '$filter', '$modal', '$popover', '$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, $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, $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); |
| }; |
| |
| const _stopTopologyRefresh = () => { |
| if ($scope.notebook && $scope.notebook.paragraphs) |
| $scope.notebook.paragraphs.forEach((paragraph) => _tryStopRefresh(paragraph)); |
| }; |
| |
| $scope.$on('$stateChangeStart', _stopTopologyRefresh); |
| |
| $scope.caches = []; |
| |
| $scope.pageSizesOptions = [ |
| {value: 50, label: '50'}, |
| {value: 100, label: '100'}, |
| {value: 200, label: '200'}, |
| {value: 400, label: '400'}, |
| {value: 800, label: '800'}, |
| {value: 1000, label: '1000'} |
| ]; |
| |
| $scope.maxPages = [ |
| {label: 'Unlimited', value: 0}, |
| {label: '1', value: 1}, |
| {label: '5', value: 5}, |
| {label: '10', value: 10}, |
| {label: '20', value: 20}, |
| {label: '50', value: 50}, |
| {label: '100', value: 100} |
| ]; |
| |
| $scope.timeLineSpans = ['1', '5', '10', '15', '30']; |
| |
| $scope.aggregateFxs = ['FIRST', 'LAST', 'MIN', 'MAX', 'SUM', 'AVG', 'COUNT']; |
| |
| $scope.modes = LegacyUtils.mkOptions(['PARTITIONED', 'REPLICATED', 'LOCAL']); |
| |
| $scope.loadingText = $root.IgniteDemoMode ? 'Demo grid is starting. Please wait...' : 'Loading query notebook screen...'; |
| |
| $scope.timeUnit = [ |
| {value: 1000, label: 'seconds', short: 's'}, |
| {value: 60000, label: 'minutes', short: 'm'}, |
| {value: 3600000, label: 'hours', short: 'h'} |
| ]; |
| |
| $scope.metadata = []; |
| |
| $scope.metaFilter = ''; |
| |
| $scope.metaOptions = { |
| nodeChildren: 'children', |
| dirSelectable: true, |
| injectClasses: { |
| iExpanded: 'fa fa-minus-square-o', |
| iCollapsed: 'fa fa-plus-square-o' |
| } |
| }; |
| |
| const maskCacheName = $filter('defaultName'); |
| |
| // We need max 1800 items to hold history for 30 mins in case of refresh every second. |
| const HISTORY_LENGTH = 1800; |
| |
| const MAX_VAL_COLS = IgniteChartColors.length; |
| |
| $anchorScroll.yOffset = 55; |
| |
| $scope.chartColor = function(index) { |
| return {color: 'white', 'background-color': IgniteChartColors[index]}; |
| }; |
| |
| function _chartNumber(arr, idx, dflt) { |
| if (idx >= 0 && arr && arr.length > idx && _.isNumber(arr[idx])) |
| return arr[idx]; |
| |
| return dflt; |
| } |
| |
| function _min(rows, idx, dflt) { |
| let min = _chartNumber(rows[0], idx, dflt); |
| |
| _.forEach(rows, (row) => { |
| const v = _chartNumber(row, idx, dflt); |
| |
| if (v < min) |
| min = v; |
| }); |
| |
| return min; |
| } |
| |
| function _max(rows, idx, dflt) { |
| let max = _chartNumber(rows[0], idx, dflt); |
| |
| _.forEach(rows, (row) => { |
| const v = _chartNumber(row, idx, dflt); |
| |
| if (v > max) |
| max = v; |
| }); |
| |
| return max; |
| } |
| |
| function _sum(rows, idx) { |
| let sum = 0; |
| |
| _.forEach(rows, (row) => sum += _chartNumber(row, idx, 0)); |
| |
| return sum; |
| } |
| |
| function _aggregate(rows, aggFx, idx, dflt) { |
| const len = rows.length; |
| |
| switch (aggFx) { |
| case 'FIRST': |
| return _chartNumber(rows[0], idx, dflt); |
| |
| case 'LAST': |
| return _chartNumber(rows[len - 1], idx, dflt); |
| |
| case 'MIN': |
| return _min(rows, idx, dflt); |
| |
| case 'MAX': |
| return _max(rows, idx, dflt); |
| |
| case 'SUM': |
| return _sum(rows, idx); |
| |
| case 'AVG': |
| return len > 0 ? _sum(rows, idx) / len : 0; |
| |
| case 'COUNT': |
| return len; |
| |
| default: |
| } |
| |
| return 0; |
| } |
| |
| function _chartLabel(arr, idx, dflt) { |
| if (arr && arr.length > idx && _.isString(arr[idx])) |
| return arr[idx]; |
| |
| return dflt; |
| } |
| |
| function _chartDatum(paragraph) { |
| let datum = []; |
| |
| if (paragraph.chartColumnsConfigured()) { |
| paragraph.chartValCols.forEach(function(valCol) { |
| let index = 0; |
| let values = []; |
| const colIdx = valCol.value; |
| |
| if (paragraph.chartTimeLineEnabled()) { |
| const aggFx = valCol.aggFx; |
| const colLbl = valCol.label + ' [' + aggFx + ']'; |
| |
| if (paragraph.charts && paragraph.charts.length === 1) |
| datum = paragraph.charts[0].data; |
| |
| const chartData = _.find(datum, {series: valCol.label}); |
| |
| const leftBound = new Date(); |
| leftBound.setMinutes(leftBound.getMinutes() - parseInt(paragraph.timeLineSpan, 10)); |
| |
| if (chartData) { |
| const lastItem = _.last(paragraph.chartHistory); |
| |
| values = chartData.values; |
| |
| values.push({ |
| x: lastItem.tm, |
| y: _aggregate(lastItem.rows, aggFx, colIdx, index++) |
| }); |
| |
| while (values.length > 0 && values[0].x < leftBound) |
| values.shift(); |
| } |
| else { |
| _.forEach(paragraph.chartHistory, (history) => { |
| if (history.tm >= leftBound) { |
| values.push({ |
| x: history.tm, |
| y: _aggregate(history.rows, aggFx, colIdx, index++) |
| }); |
| } |
| }); |
| |
| datum.push({series: valCol.label, key: colLbl, values}); |
| } |
| } |
| else { |
| index = paragraph.total; |
| |
| values = _.map(paragraph.rows, function(row) { |
| const xCol = paragraph.chartKeyCols[0].value; |
| |
| const v = { |
| x: _chartNumber(row, xCol, index), |
| xLbl: _chartLabel(row, xCol, null), |
| y: _chartNumber(row, colIdx, index) |
| }; |
| |
| index++; |
| |
| return v; |
| }); |
| |
| datum.push({series: valCol.label, key: valCol.label, values}); |
| } |
| }); |
| } |
| |
| return datum; |
| } |
| |
| function _xX(d) { |
| return d.x; |
| } |
| |
| function _yY(d) { |
| return d.y; |
| } |
| |
| function _xAxisTimeFormat(d) { |
| return d3.time.format('%X')(new Date(d)); |
| } |
| |
| const _intClasses = ['java.lang.Byte', 'java.lang.Integer', 'java.lang.Long', 'java.lang.Short']; |
| |
| function _intType(cls) { |
| return _.includes(_intClasses, cls); |
| } |
| |
| const _xAxisWithLabelFormat = function(paragraph) { |
| return function(d) { |
| const values = paragraph.charts[0].data[0].values; |
| |
| const fmt = _intType(paragraph.chartKeyCols[0].type) ? 'd' : ',.2f'; |
| |
| const dx = values[d]; |
| |
| if (!dx) |
| return d3.format(fmt)(d); |
| |
| const lbl = dx.xLbl; |
| |
| return lbl ? lbl : d3.format(fmt)(d); |
| }; |
| }; |
| |
| function _xAxisLabel(paragraph) { |
| return _.isEmpty(paragraph.chartKeyCols) ? 'X' : paragraph.chartKeyCols[0].label; |
| } |
| |
| const _yAxisFormat = function(d) { |
| const fmt = d < 1000 ? ',.2f' : '.3s'; |
| |
| return d3.format(fmt)(d); |
| }; |
| |
| function _updateCharts(paragraph) { |
| $timeout(() => _.forEach(paragraph.charts, (chart) => chart.api.update()), 100); |
| } |
| |
| function _updateChartsWithData(paragraph, newDatum) { |
| $timeout(() => { |
| if (!paragraph.chartTimeLineEnabled()) { |
| const chartDatum = paragraph.charts[0].data; |
| |
| chartDatum.length = 0; |
| |
| _.forEach(newDatum, (series) => chartDatum.push(series)); |
| } |
| |
| paragraph.charts[0].api.update(); |
| }); |
| } |
| |
| function _yAxisLabel(paragraph) { |
| const cols = paragraph.chartValCols; |
| |
| const tml = paragraph.chartTimeLineEnabled(); |
| |
| return _.isEmpty(cols) ? 'Y' : _.map(cols, function(col) { |
| let lbl = col.label; |
| |
| if (tml) |
| lbl += ' [' + col.aggFx + ']'; |
| |
| return lbl; |
| }).join(', '); |
| } |
| |
| function _barChart(paragraph) { |
| const datum = _chartDatum(paragraph); |
| |
| if (_.isEmpty(paragraph.charts)) { |
| const stacked = paragraph.chartsOptions && paragraph.chartsOptions.barChart |
| ? paragraph.chartsOptions.barChart.stacked |
| : true; |
| |
| const options = { |
| chart: { |
| type: 'multiBarChart', |
| height: 400, |
| margin: {left: 70}, |
| duration: 0, |
| x: _xX, |
| y: _yY, |
| xAxis: { |
| axisLabel: _xAxisLabel(paragraph), |
| tickFormat: paragraph.chartTimeLineEnabled() ? _xAxisTimeFormat : _xAxisWithLabelFormat(paragraph), |
| showMaxMin: false |
| }, |
| yAxis: { |
| axisLabel: _yAxisLabel(paragraph), |
| tickFormat: _yAxisFormat |
| }, |
| color: IgniteChartColors, |
| stacked, |
| showControls: true, |
| legend: { |
| vers: 'furious', |
| margin: {right: -15} |
| } |
| } |
| }; |
| |
| paragraph.charts = [{options, data: datum}]; |
| |
| _updateCharts(paragraph); |
| } |
| else |
| _updateChartsWithData(paragraph, datum); |
| } |
| |
| function _pieChartDatum(paragraph) { |
| const datum = []; |
| |
| if (paragraph.chartColumnsConfigured() && !paragraph.chartTimeLineEnabled()) { |
| paragraph.chartValCols.forEach(function(valCol) { |
| let index = paragraph.total; |
| |
| const values = _.map(paragraph.rows, (row) => { |
| const xCol = paragraph.chartKeyCols[0].value; |
| |
| const v = { |
| x: xCol < 0 ? index : row[xCol], |
| y: _chartNumber(row, valCol.value, index) |
| }; |
| |
| // Workaround for known problem with zero values on Pie chart. |
| if (v.y === 0) |
| v.y = 0.0001; |
| |
| index++; |
| |
| return v; |
| }); |
| |
| datum.push({series: paragraph.chartKeyCols[0].label, key: valCol.label, values}); |
| }); |
| } |
| |
| return datum; |
| } |
| |
| function _pieChart(paragraph) { |
| let datum = _pieChartDatum(paragraph); |
| |
| if (datum.length === 0) |
| datum = [{values: []}]; |
| |
| paragraph.charts = _.map(datum, function(data) { |
| return { |
| options: { |
| chart: { |
| type: 'pieChart', |
| height: 400, |
| duration: 0, |
| x: _xX, |
| y: _yY, |
| showLabels: true, |
| labelThreshold: 0.05, |
| labelType: 'percent', |
| donut: true, |
| donutRatio: 0.35, |
| legend: { |
| vers: 'furious', |
| margin: {right: -15} |
| } |
| }, |
| title: { |
| enable: true, |
| text: data.key |
| } |
| }, |
| data: data.values |
| }; |
| }); |
| |
| _updateCharts(paragraph); |
| } |
| |
| function _lineChart(paragraph) { |
| const datum = _chartDatum(paragraph); |
| |
| if (_.isEmpty(paragraph.charts)) { |
| const options = { |
| chart: { |
| type: 'lineChart', |
| height: 400, |
| margin: { left: 70 }, |
| duration: 0, |
| x: _xX, |
| y: _yY, |
| xAxis: { |
| axisLabel: _xAxisLabel(paragraph), |
| tickFormat: paragraph.chartTimeLineEnabled() ? _xAxisTimeFormat : _xAxisWithLabelFormat(paragraph), |
| showMaxMin: false |
| }, |
| yAxis: { |
| axisLabel: _yAxisLabel(paragraph), |
| tickFormat: _yAxisFormat |
| }, |
| color: IgniteChartColors, |
| useInteractiveGuideline: true, |
| legend: { |
| vers: 'furious', |
| margin: {right: -15} |
| } |
| } |
| }; |
| |
| paragraph.charts = [{options, data: datum}]; |
| |
| _updateCharts(paragraph); |
| } |
| else |
| _updateChartsWithData(paragraph, datum); |
| } |
| |
| function _areaChart(paragraph) { |
| const datum = _chartDatum(paragraph); |
| |
| if (_.isEmpty(paragraph.charts)) { |
| const style = paragraph.chartsOptions && paragraph.chartsOptions.areaChart |
| ? paragraph.chartsOptions.areaChart.style |
| : 'stack'; |
| |
| const options = { |
| chart: { |
| type: 'stackedAreaChart', |
| height: 400, |
| margin: {left: 70}, |
| duration: 0, |
| x: _xX, |
| y: _yY, |
| xAxis: { |
| axisLabel: _xAxisLabel(paragraph), |
| tickFormat: paragraph.chartTimeLineEnabled() ? _xAxisTimeFormat : _xAxisWithLabelFormat(paragraph), |
| showMaxMin: false |
| }, |
| yAxis: { |
| axisLabel: _yAxisLabel(paragraph), |
| tickFormat: _yAxisFormat |
| }, |
| color: IgniteChartColors, |
| style, |
| legend: { |
| vers: 'furious', |
| margin: {right: -15} |
| } |
| } |
| }; |
| |
| paragraph.charts = [{options, data: datum}]; |
| |
| _updateCharts(paragraph); |
| } |
| else |
| _updateChartsWithData(paragraph, datum); |
| } |
| |
| function _chartApplySettings(paragraph, resetCharts) { |
| if (resetCharts) |
| paragraph.charts = []; |
| |
| if (paragraph.chart() && paragraph.nonEmpty()) { |
| switch (paragraph.result) { |
| case 'bar': |
| _barChart(paragraph); |
| break; |
| |
| case 'pie': |
| _pieChart(paragraph); |
| break; |
| |
| case 'line': |
| _lineChart(paragraph); |
| break; |
| |
| case 'area': |
| _areaChart(paragraph); |
| break; |
| |
| default: |
| } |
| } |
| } |
| |
| $scope.chartRemoveKeyColumn = function(paragraph, index) { |
| paragraph.chartKeyCols.splice(index, 1); |
| |
| _chartApplySettings(paragraph, true); |
| }; |
| |
| $scope.chartRemoveValColumn = function(paragraph, index) { |
| paragraph.chartValCols.splice(index, 1); |
| |
| _chartApplySettings(paragraph, true); |
| }; |
| |
| $scope.chartAcceptKeyColumn = function(paragraph, item) { |
| const accepted = _.findIndex(paragraph.chartKeyCols, item) < 0; |
| |
| if (accepted) { |
| paragraph.chartKeyCols = [item]; |
| |
| _chartApplySettings(paragraph, true); |
| } |
| |
| return false; |
| }; |
| |
| const _numberClasses = ['java.math.BigDecimal', 'java.lang.Byte', 'java.lang.Double', |
| 'java.lang.Float', 'java.lang.Integer', 'java.lang.Long', 'java.lang.Short']; |
| |
| const _numberType = function(cls) { |
| return _.includes(_numberClasses, cls); |
| }; |
| |
| $scope.chartAcceptValColumn = function(paragraph, item) { |
| const valCols = paragraph.chartValCols; |
| |
| const accepted = _.findIndex(valCols, item) < 0 && item.value >= 0 && _numberType(item.type); |
| |
| if (accepted) { |
| if (valCols.length === MAX_VAL_COLS - 1) |
| valCols.shift(); |
| |
| valCols.push(item); |
| |
| _chartApplySettings(paragraph, true); |
| } |
| |
| return false; |
| }; |
| |
| $scope.scrollParagraphs = []; |
| |
| $scope.rebuildScrollParagraphs = function() { |
| $scope.scrollParagraphs = $scope.notebook.paragraphs.map(function(paragraph) { |
| return { |
| text: paragraph.name, |
| click: 'scrollToParagraph("' + paragraph.id + '")' |
| }; |
| }); |
| }; |
| |
| $scope.scrollToParagraph = (id) => { |
| const idx = _.findIndex($scope.notebook.paragraphs, {id}); |
| |
| if (idx >= 0) { |
| if (!_.includes($scope.notebook.expandedParagraphs, idx)) |
| $scope.notebook.expandedParagraphs = $scope.notebook.expandedParagraphs.concat([idx]); |
| |
| if ($scope.notebook.paragraphs[idx].ace) |
| setTimeout(() => $scope.notebook.paragraphs[idx].ace.focus()); |
| } |
| |
| $location.hash(id); |
| |
| $anchorScroll(); |
| }; |
| |
| const _hideColumn = (col) => col.fieldName !== '_KEY' && col.fieldName !== '_VAL'; |
| |
| const _allColumn = () => true; |
| |
| $scope.aceInit = function(paragraph) { |
| return function(editor) { |
| editor.setAutoScrollEditorIntoView(true); |
| editor.$blockScrolling = Infinity; |
| |
| const renderer = editor.renderer; |
| |
| renderer.setHighlightGutterLine(false); |
| renderer.setShowPrintMargin(false); |
| renderer.setOption('fontFamily', 'monospace'); |
| renderer.setOption('fontSize', '14px'); |
| renderer.setOption('minLines', '5'); |
| renderer.setOption('maxLines', '15'); |
| |
| editor.setTheme('ace/theme/chrome'); |
| |
| Object.defineProperty(paragraph, 'ace', { value: editor }); |
| }; |
| }; |
| |
| /** |
| * Update caches list. |
| */ |
| const _refreshCaches = () => { |
| return agentMgr.publicCacheNames() |
| .then((cacheNames) => { |
| $scope.caches = _.sortBy(_.map(cacheNames, (name) => ({ |
| label: maskCacheName(name, true), |
| value: name |
| })), (cache) => cache.label.toLowerCase()); |
| |
| _.forEach($scope.notebook.paragraphs, (paragraph) => { |
| if (!_.includes(cacheNames, paragraph.cacheName)) |
| paragraph.cacheName = _.head(cacheNames); |
| }); |
| |
| // Await for demo caches. |
| if (!$ctrl.demoStarted && $root.IgniteDemoMode && nonEmpty(cacheNames)) { |
| $ctrl.demoStarted = true; |
| |
| Loading.finish('sqlLoading'); |
| |
| _.forEach($scope.notebook.paragraphs, (paragraph) => $scope.execute(paragraph)); |
| } |
| |
| $scope.$applyAsync(); |
| }) |
| .catch((err) => Messages.showError(err)); |
| }; |
| |
| const _startWatch = () => { |
| const 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); |
| }; |
| |
| const _showLoading = (paragraph, enable) => { |
| if (paragraph.qryType === 'scan') |
| paragraph.scanningInProgress = enable; |
| |
| paragraph.loading = enable; |
| }; |
| |
| const _fetchQueryResult = (paragraph, clearChart, res, qryArg) => { |
| if (!_.isNil(res.rows)) { |
| _processQueryResult(paragraph, clearChart, res); |
| _tryStartRefresh(paragraph); |
| |
| $scope.$applyAsync(); |
| |
| return; |
| } |
| |
| const subscription = timer(100, 500).pipe( |
| exhaustMap(() => agentMgr.queryFetchFistsPage(qryArg.nid, res.queryId, qryArg.pageSize)), |
| filter((res) => !_.isNil(res.rows)), |
| take(1), |
| map((res) => _fetchQueryResult(paragraph, false, res, qryArg)), |
| catchError((err) => { |
| if (paragraph.subscription) { |
| paragraph.subscription.unsubscribe(); |
| paragraph.setError(err); |
| |
| _showLoading(paragraph, false); |
| delete paragraph.subscription; |
| |
| $scope.$applyAsync(); |
| } |
| |
| return of(err); |
| }) |
| ).subscribe(); |
| |
| Object.defineProperty(paragraph, 'subscription', {value: subscription, configurable: true}); |
| |
| return subscription; |
| }; |
| |
| /** |
| * @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 (paragraph.subscription) { |
| paragraph.subscription.unsubscribe(); |
| delete paragraph.subscription; |
| } |
| |
| 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) { |
| const qryId = paragraph.queryId; |
| delete paragraph.queryId; |
| |
| return agentMgr.queryClose(nid, qryId); |
| } |
| |
| return $q.when(); |
| }; |
| |
| $scope.cancelQuery = (paragraph) => { |
| if (paragraph.subscription) { |
| paragraph.subscription.unsubscribe(); |
| delete paragraph.subscription; |
| } |
| |
| _showLoading(paragraph, false); |
| this.$scope.stopRefresh(paragraph); |
| |
| _closeOldQuery(paragraph) |
| .then(() => _showLoading(paragraph, false)) |
| .catch((err) => { |
| _showLoading(paragraph, false); |
| paragraph.setError(err); |
| }); |
| }; |
| |
| $scope.cancelQueryAvailable = (paragraph) => { |
| return !!paragraph.subscription; |
| }; |
| |
| /** |
| * @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; |
| |
| agentMgr.awaitCluster() |
| .then(() => _closeOldQuery(paragraph)) |
| .then(() => args.localNid || _chooseNode(args.cacheName, false)) |
| .then((nid) => { |
| 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 agentMgr.querySql(qryArg) |
| .then((res) => _fetchQueryResult(paragraph, false, res, qryArg)); |
| }) |
| .catch((err) => paragraph.setError(err)); |
| }; |
| |
| const _tryStartRefresh = function(paragraph) { |
| _tryStopRefresh(paragraph); |
| |
| if (_.get(paragraph, 'rate.installed') && paragraph.queryExecuted()) { |
| $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; |
| |
| paragraph.rows = []; |
| paragraph.gridOptions.adjustHeight(paragraph.rows.length); |
| |
| paragraph.meta = []; |
| paragraph.setError({message: ''}); |
| |
| paragraph.hasNext = false; |
| }; |
| |
| $scope.execute = (paragraph, local = false) => { |
| const nonCollocatedJoins = !!paragraph.nonCollocatedJoins; |
| const enforceJoinOrder = !!paragraph.enforceJoinOrder; |
| const lazy = !!paragraph.lazy; |
| const collocated = !!paragraph.collocated; |
| |
| $scope.queryAvailable(paragraph) && _chooseNode(paragraph.cacheName, local) |
| .then((nid) => { |
| // If we are executing only selected part of query then Notebook shouldn't be saved. |
| if (!paragraph.partialQuery) |
| Notebook.save($scope.notebook).catch(Messages.showError); |
| |
| paragraph.localQueryMode = local; |
| paragraph.prevQuery = paragraph.queryArgs ? paragraph.queryArgs.query : paragraph.query; |
| |
| _showLoading(paragraph, true); |
| |
| return _closeOldQuery(paragraph) |
| .then(() => { |
| const query = paragraph.partialQuery || paragraph.query; |
| |
| const args = paragraph.queryArgs = { |
| type: 'QUERY', |
| cacheName: $scope.cacheNameForSql(paragraph), |
| query, |
| pageSize: paragraph.pageSize, |
| maxPages: paragraph.maxPages, |
| nonCollocatedJoins, |
| enforceJoinOrder, |
| localNid: local ? nid : null, |
| lazy, |
| collocated |
| }; |
| |
| ActivitiesData.post({ group: 'sql', action: '/queries/execute' }); |
| |
| const qry = args.maxPages ? addLimit(args.query, args.pageSize * args.maxPages) : query; |
| const qryArg = { |
| nid, |
| cacheName: args.cacheName, |
| query: qry, |
| nonCollocatedJoins, |
| enforceJoinOrder, |
| replicatedOnly: false, |
| local, |
| pageSize: args.pageSize, |
| lazy, |
| collocated |
| }; |
| |
| return agentMgr.querySql(qryArg) |
| .then((res) => { |
| _initQueryResult(paragraph, res); |
| |
| return _fetchQueryResult(paragraph, true, res, qryArg); |
| }); |
| }) |
| .catch((err) => { |
| paragraph.setError(err); |
| |
| _showLoading(paragraph, false); |
| |
| $scope.stopRefresh(paragraph); |
| |
| Messages.showError(err); |
| }) |
| .then(() => paragraph.ace.focus()); |
| }); |
| }; |
| |
| const _cancelRefresh = (paragraph) => { |
| if (paragraph.rate && paragraph.rate.stopTime) { |
| delete paragraph.queryArgs; |
| |
| paragraph.rate.installed = false; |
| |
| $interval.cancel(paragraph.rate.stopTime); |
| |
| delete paragraph.rate.stopTime; |
| } |
| }; |
| |
| $scope.explain = (paragraph) => { |
| const nonCollocatedJoins = !!paragraph.nonCollocatedJoins; |
| const enforceJoinOrder = !!paragraph.enforceJoinOrder; |
| const collocated = !!paragraph.collocated; |
| |
| if (!$scope.queryAvailable(paragraph)) |
| return; |
| |
| if (!paragraph.partialQuery) |
| Notebook.save($scope.notebook).catch(Messages.showError); |
| |
| _cancelRefresh(paragraph); |
| |
| _showLoading(paragraph, true); |
| |
| _closeOldQuery(paragraph) |
| .then(() => _chooseNode(paragraph.cacheName, false)) |
| .then((nid) => { |
| const args = paragraph.queryArgs = { |
| type: 'EXPLAIN', |
| cacheName: $scope.cacheNameForSql(paragraph), |
| query: 'EXPLAIN ' + (paragraph.partialQuery || paragraph.query), |
| pageSize: paragraph.pageSize |
| }; |
| |
| ActivitiesData.post({ group: 'sql', action: '/queries/explain' }); |
| |
| const qryArg = { |
| nid, |
| cacheName: args.cacheName, |
| query: args.query, |
| nonCollocatedJoins, |
| enforceJoinOrder, |
| replicatedOnly: false, |
| local: false, |
| pageSize: args.pageSize, |
| lazy: false, collocated |
| }; |
| |
| return agentMgr.querySql(qryArg) |
| .then((res) => { |
| _initQueryResult(paragraph, res); |
| |
| return _fetchQueryResult(paragraph, true, res, qryArg); |
| }); |
| }) |
| .catch((err) => { |
| paragraph.setError(err); |
| |
| _showLoading(paragraph, false); |
| }) |
| .then(() => paragraph.ace.focus()); |
| }; |
| |
| $scope.scan = (paragraph, local = false) => { |
| const cacheName = paragraph.cacheName; |
| const caseSensitive = !!paragraph.caseSensitive; |
| const filter = paragraph.filter; |
| const pageSize = paragraph.pageSize; |
| |
| $scope.scanAvailable(paragraph) && _chooseNode(cacheName, local) |
| .then((nid) => { |
| paragraph.localQueryMode = local; |
| |
| Notebook.save($scope.notebook) |
| .catch(Messages.showError); |
| |
| _cancelRefresh(paragraph); |
| |
| _showLoading(paragraph, true); |
| |
| _closeOldQuery(paragraph) |
| .then(() => { |
| paragraph.queryArgs = { |
| type: 'SCAN', |
| cacheName, |
| filter, |
| regEx: false, |
| caseSensitive, |
| near: false, |
| pageSize, |
| localNid: local ? nid : null |
| }; |
| |
| ActivitiesData.post({ group: 'sql', action: '/queries/scan' }); |
| |
| const qryArg = { |
| nid, |
| cacheName, |
| filter, |
| regEx: false, |
| caseSensitive, |
| near: false, |
| local, |
| pageSize |
| }; |
| |
| return agentMgr.queryScan(qryArg).then((res) => { |
| _initQueryResult(paragraph, res); |
| |
| return _fetchQueryResult(paragraph, true, res, qryArg); |
| }); |
| }) |
| .catch((err) => { |
| paragraph.setError(err); |
| |
| _showLoading(paragraph, false); |
| }); |
| }); |
| }; |
| |
| function _updatePieChartsWithData(paragraph, newDatum) { |
| $timeout(() => { |
| _.forEach(paragraph.charts, function(chart) { |
| const chartDatum = chart.data; |
| |
| chartDatum.length = 0; |
| |
| _.forEach(newDatum, function(series) { |
| if (chart.options.title.text === series.key) |
| _.forEach(series.values, (v) => chartDatum.push(v)); |
| }); |
| }); |
| |
| _.forEach(paragraph.charts, (chart) => chart.api.update()); |
| }); |
| } |
| |
| $scope.nextPage = (paragraph) => { |
| _showLoading(paragraph, true); |
| |
| paragraph.queryArgs.pageSize = paragraph.pageSize; |
| |
| agentMgr.queryNextPage(paragraph.resNodeId, paragraph.queryId, paragraph.pageSize) |
| .then((res) => { |
| paragraph.page++; |
| |
| paragraph.total += paragraph.rows.length; |
| |
| paragraph.duration = res.duration; |
| |
| paragraph.rows = res.rows; |
| |
| if (paragraph.chart()) { |
| if (paragraph.result === 'pie') |
| _updatePieChartsWithData(paragraph, _pieChartDatum(paragraph)); |
| else |
| _updateChartsWithData(paragraph, _chartDatum(paragraph)); |
| } |
| |
| paragraph.gridOptions.adjustHeight(paragraph.rows.length); |
| |
| _showLoading(paragraph, false); |
| |
| if (!res.hasMore) |
| delete paragraph.queryId; |
| }) |
| .catch((err) => { |
| paragraph.setError(err); |
| |
| _showLoading(paragraph, false); |
| }) |
| .then(() => paragraph.ace && paragraph.ace.focus()); |
| }; |
| |
| const _export = (fileName, columnDefs, meta, rows, toClipBoard = false) => { |
| const csvSeparator = this.CSV.getSeparator(); |
| let csvContent = ''; |
| |
| const cols = []; |
| const excludedCols = []; |
| |
| _.forEach(meta, (col, idx) => { |
| if (columnDefs[idx].visible) |
| cols.push(_fullColName(col)); |
| else |
| excludedCols.push(idx); |
| }); |
| |
| csvContent += cols.join(csvSeparator) + '\n'; |
| |
| _.forEach(rows, (row) => { |
| cols.length = 0; |
| |
| if (Array.isArray(row)) { |
| _.forEach(row, (elem, idx) => { |
| if (_.includes(excludedCols, idx)) |
| return; |
| |
| cols.push(_.isUndefined(elem) ? '' : JSON.stringify(elem)); |
| }); |
| } |
| else { |
| _.forEach(columnDefs, (col) => { |
| if (col.visible) { |
| const elem = row[col.fieldName]; |
| |
| cols.push(_.isUndefined(elem) ? '' : JSON.stringify(elem)); |
| } |
| }); |
| } |
| |
| csvContent += cols.join(csvSeparator) + '\n'; |
| }); |
| |
| if (toClipBoard) |
| IgniteCopyToClipboard.copy(csvContent); |
| else |
| LegacyUtils.download('text/csv', fileName, csvContent); |
| }; |
| |
| /** |
| * Generate file name with query results. |
| * |
| * @param paragraph {Object} Query paragraph . |
| * @param all {Boolean} All result export flag. |
| * @returns {string} |
| */ |
| const exportFileName = (paragraph, all) => { |
| const args = paragraph.queryArgs; |
| |
| if (args.type === 'SCAN') |
| return `export-scan-${args.cacheName}-${paragraph.name}${all ? '-all' : ''}.csv`; |
| |
| return `export-query-${paragraph.name}${all ? '-all' : ''}.csv`; |
| }; |
| |
| $scope.exportCsvToClipBoard = (paragraph) => { |
| _export(exportFileName(paragraph, false), paragraph.gridOptions.columnDefs, paragraph.meta, paragraph.rows, true); |
| }; |
| |
| $scope.exportCsv = function(paragraph) { |
| _export(exportFileName(paragraph, false), paragraph.gridOptions.columnDefs, paragraph.meta, paragraph.rows); |
| |
| // paragraph.gridOptions.api.exporter.csvExport(uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE); |
| }; |
| |
| $scope.exportPdf = function(paragraph) { |
| paragraph.gridOptions.api.exporter.pdfExport(uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE); |
| }; |
| |
| $scope.exportCsvAll = (paragraph) => { |
| paragraph.csvIsPreparing = true; |
| |
| const args = paragraph.queryArgs; |
| |
| return Promise.resolve(args.localNid || _chooseNode(args.cacheName, false)) |
| .then((nid) => args.type === 'SCAN' |
| ? agentMgr.queryScanGetAll({ |
| nid, |
| cacheName: args.cacheName, |
| filter: args.query, |
| regEx: !!args.regEx, |
| caseSensitive: !!args.caseSensitive, |
| near: !!args.near, |
| local: !!args.localNid |
| }) |
| : agentMgr.querySqlGetAll({ |
| nid, |
| cacheName: args.cacheName, |
| query: args.query, |
| nonCollocatedJoins: !!args.nonCollocatedJoins, |
| enforceJoinOrder: !!args.enforceJoinOrder, |
| replicatedOnly: false, |
| local: !!args.localNid, |
| lazy: !!args.lazy, |
| collocated: !!args.collocated |
| })) |
| .then((res) => _export(exportFileName(paragraph, true), paragraph.gridOptions.columnDefs, res.columns, res.rows)) |
| .catch(Messages.showError) |
| .then(() => { |
| paragraph.csvIsPreparing = false; |
| |
| return paragraph.ace && paragraph.ace.focus(); |
| }); |
| }; |
| |
| // $scope.exportPdfAll = function(paragraph) { |
| // $http.post('/api/v1/agent/query/getAll', {query: paragraph.query, cacheName: paragraph.cacheName}) |
| // .then(({data}) { |
| // _export(paragraph.name + '-all.csv', data.meta, data.rows); |
| // }) |
| // .catch(Messages.showError); |
| // }; |
| |
| $scope.rateAsString = function(paragraph) { |
| if (paragraph.rate && paragraph.rate.installed) { |
| const idx = _.findIndex($scope.timeUnit, function(unit) { |
| return unit.value === paragraph.rate.unit; |
| }); |
| |
| if (idx >= 0) |
| return ' ' + paragraph.rate.value + $scope.timeUnit[idx].short; |
| |
| paragraph.rate.installed = false; |
| } |
| |
| return ''; |
| }; |
| |
| $scope.startRefresh = function(paragraph, value, unit) { |
| paragraph.rate.value = value; |
| paragraph.rate.unit = unit; |
| paragraph.rate.installed = true; |
| |
| if (paragraph.queryExecuted() && !paragraph.scanExplain()) |
| _executeRefresh(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(), 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.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)) { |
| 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}); |
| } |
| }; |
| |
| $window.addEventListener('beforeunload', () => { |
| this._closeOpenedQueries(this.$scope.notebook.paragraphs); |
| }); |
| } |
| |
| _closeOpenedQueries(paragraphs) { |
| _.forEach(paragraphs, ({queryId, subscription, resNodeId}) => { |
| if (subscription) |
| subscription.unsubscribe(); |
| |
| if (queryId) { |
| this.agentMgr.queryClose(resNodeId, queryId) |
| .catch(() => { /* No-op. */ }); |
| } |
| }); |
| } |
| |
| scanActions: QueryActions<Paragraph & {type: 'scan'}> = [ |
| { |
| text: 'Scan', |
| click: (p) => this.$scope.scan(p), |
| available: (p) => this.$scope.scanAvailable(p) |
| }, |
| { |
| text: 'Scan on selected node', |
| click: (p) => this.$scope.scan(p, true), |
| available: (p) => this.$scope.scanAvailable(p) |
| }, |
| {text: 'Rename', click: (p) => this.renameParagraph(p), available: () => true}, |
| {text: 'Remove', click: (p) => this.removeParagraph(p), available: () => true} |
| ]; |
| |
| queryActions: QueryActions<Paragraph & {type: 'query'}> = [ |
| { |
| text: 'Execute', |
| click: (p) => this.$scope.execute(p), |
| available: (p) => this.$scope.queryAvailable(p) |
| }, |
| { |
| text: 'Execute on selected node', |
| click: (p) => this.$scope.execute(p, true), |
| available: (p) => this.$scope.queryAvailable(p) |
| }, |
| { |
| text: 'Explain', |
| click: (p) => this.$scope.explain(p), |
| available: (p) => this.$scope.queryAvailable(p) |
| }, |
| {text: 'Rename', click: (p) => this.renameParagraph(p), available: () => true}, |
| {text: 'Remove', click: (p) => this.removeParagraph(p), available: () => true} |
| ]; |
| |
| async renameParagraph(paragraph: Paragraph) { |
| try { |
| const newName = await this.IgniteInput.input('Rename Query', 'New query name:', paragraph.name); |
| |
| if (paragraph.name !== newName) { |
| paragraph.name = newName; |
| |
| this.$scope.rebuildScrollParagraphs(); |
| |
| await this.Notebook.save(this.$scope.notebook) |
| .catch(this.Messages.showError); |
| } |
| } |
| catch (ignored) { |
| // No-op. |
| } |
| } |
| |
| async removeParagraph(paragraph: Paragraph) { |
| try { |
| await this.Confirm.confirm('Are you sure you want to remove query: "' + paragraph.name + '"?'); |
| this.$scope.stopRefresh(paragraph); |
| |
| 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(); |
| |
| 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() { |
| this._closeOpenedQueries(this.$scope.notebook.paragraphs); |
| |
| if (this.subscribers$) |
| this.subscribers$.unsubscribe(); |
| } |
| } |