This is an automated email from the ASF dual-hosted git repository.
anovikov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git
The following commit(s) were added to refs/heads/master by this push:
new c2da3d5 IGNITE-11035 Updates in queries page - Fixes #6021.
c2da3d5 is described below
commit c2da3d52fea8fd7661bd4d1b3823e2d77c2b0aa1
Author: vsisko <[email protected]>
AuthorDate: Mon Mar 11 18:06:20 2019 +0700
IGNITE-11035 Updates in queries page - Fixes #6021.
---
modules/web-console/backend/index.js | 0
.../app/components/cluster-selector/controller.js | 10 +-
.../app/components/cluster-selector/template.pug | 4 +-
.../components/queries-notebook/controller.ts | 649 +++++++++++++--------
.../components/queries-notebook/template.tpl.pug | 4 +-
.../app/modules/agent/AgentManager.service.js | 105 +---
6 files changed, 437 insertions(+), 335 deletions(-)
diff --git a/modules/web-console/backend/index.js
b/modules/web-console/backend/index.js
old mode 100644
new mode 100755
diff --git
a/modules/web-console/frontend/app/components/cluster-selector/controller.js
b/modules/web-console/frontend/app/components/cluster-selector/controller.js
index 484a5eb..98e9043 100644
--- a/modules/web-console/frontend/app/components/cluster-selector/controller.js
+++ b/modules/web-console/frontend/app/components/cluster-selector/controller.js
@@ -19,6 +19,7 @@ import _ from 'lodash';
import { BehaviorSubject } from 'rxjs';
import {tap, filter, combineLatest} from 'rxjs/operators';
+import {CancellationError} from 'app/errors/CancellationError';
export default class {
static $inject = ['AgentManager', 'IgniteConfirm', 'IgniteVersion',
'IgniteMessages'];
@@ -63,8 +64,13 @@ export default class {
this.clusters$.unsubscribe();
}
- change() {
- this.agentMgr.switchCluster(this.cluster);
+ change(item) {
+ this.agentMgr.switchCluster(this.cluster)
+ .then(() => this.cluster = item)
+ .catch((err) => {
+ if (!(err instanceof CancellationError))
+ this.Messages.showError('Failed to switch cluster: ', err);
+ });
}
isChangeStateAvailable() {
diff --git
a/modules/web-console/frontend/app/components/cluster-selector/template.pug
b/modules/web-console/frontend/app/components/cluster-selector/template.pug
index 39c1af2..8f34ca6 100644
--- a/modules/web-console/frontend/app/components/cluster-selector/template.pug
+++ b/modules/web-console/frontend/app/components/cluster-selector/template.pug
@@ -34,8 +34,6 @@ button.btn-ignite.btn-ignite--primary(
span(data-ng-if='!$ctrl.isDemo && $ctrl.clusters.length > 1')
div.btn-ignite.btn-ignite--primary(
- ng-model='$ctrl.cluster'
-
bs-dropdown=''
data-trigger='click'
data-container='body'
@@ -53,7 +51,7 @@ span(data-ng-if='!$ctrl.isDemo && $ctrl.clusters.length > 1')
ul.bs-select-menu.dropdown-menu(role='menu')
li(ng-repeat='item in $ctrl.clusters')
- button.btn-ignite.bssm-item-button(ng-click='$ctrl.cluster = item;
$ctrl.change()')
+ button.btn-ignite.bssm-item-button(ng-click='$ctrl.change(item)')
span.icon-left
svg(ignite-icon='{{ item.secured ? "lockClosed" :
"lockOpened" }}')
| {{ item.name }}
diff --git
a/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/controller.ts
b/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/controller.ts
index 7a96a91..1bb1437 100644
---
a/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/controller.ts
+++
b/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/controller.ts
@@ -18,8 +18,8 @@
import _ from 'lodash';
import {nonEmpty, nonNil} from 'app/utils/lodashMixins';
import id8 from 'app/utils/id8';
-import {timer, merge, defer, of, EMPTY, from} from 'rxjs';
-import {tap, switchMap, exhaustMap, take, pluck, distinctUntilChanged, filter,
map, catchError} from 'rxjs/operators';
+import {Subject, defer, from, of, merge, timer, EMPTY} from 'rxjs';
+import {catchError, distinctUntilChanged, expand, exhaustMap, filter,
finalize, first, map, mergeMap, pluck, switchMap, takeUntil, take, tap} from
'rxjs/operators';
import {CSV} from 'app/services/CSV';
@@ -33,6 +33,7 @@ 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'};
@@ -82,6 +83,9 @@ class Paragraph {
self.csvIsPreparing = false;
self.scanningInProgress = false;
+ self.cancelQuerySubject = new Subject();
+ self.cancelExportSubject = new Subject();
+
_.assign(this, paragraph);
Object.defineProperty(this, 'gridOptions', {value: {
@@ -148,6 +152,13 @@ class Paragraph {
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);
@@ -208,7 +219,7 @@ class Paragraph {
}
scanExplain() {
- return this.queryExecuted() && this.queryArgs.type !== 'QUERY';
+ return this.queryExecuted() && (this.qryType === 'scan' ||
this.queryArgs.query.startsWith('EXPLAIN '));
}
timeLineSupported() {
@@ -251,20 +262,40 @@ class Paragraph {
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', '$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'];
+ 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', 'D [...]
/**
* @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, errorP [...]
+ 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, IgniteCopyToClipboar [...]
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 });
+ 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;
@@ -1233,46 +1264,99 @@ export class NotebookCtrl {
_rebuildColumns(paragraph);
};
- const _showLoading = (paragraph, enable) => {
- if (paragraph.qryType === 'scan')
- paragraph.scanningInProgress = enable;
-
- paragraph.loading = enable;
+ /**
+ * 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();
+ }),
+ expand((res) => timer(100, 500).pipe(
+ exhaustMap(() => agentMgr.queryFetchFistsPage(qryArg.nid,
res.queryId, qryArg.pageSize)),
+ filter((res) => !_.isNil(res.rows))
+ )),
+ filter((res) => !_.isNil(res.rows)),
+ first()
+ );
};
- 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)),
+ /**
+ * 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) => {
- if (paragraph.subscription) {
- paragraph.subscription.unsubscribe();
- paragraph.setError(err);
-
- _showLoading(paragraph, false);
- delete paragraph.subscription;
-
- $scope.$applyAsync();
- }
+ onError(err);
+ $scope.$applyAsync();
return of(err);
})
- ).subscribe();
+ );
+ };
- Object.defineProperty(paragraph, 'subscription', {value:
subscription, configurable: true});
+ /**
+ * 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 subscription;
+ return of(err);
+ })
+ );
};
/**
@@ -1285,11 +1369,6 @@ export class NotebookCtrl {
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 = [];
@@ -1360,7 +1439,7 @@ export class NotebookCtrl {
while (chartHistory.length > HISTORY_LENGTH)
chartHistory.shift();
- _showLoading(paragraph, false);
+ paragraph.showLoading(false);
if (_.isNil(paragraph.result) || paragraph.result === 'none' ||
paragraph.scanExplain())
paragraph.result = 'table';
@@ -1380,6 +1459,11 @@ export class NotebookCtrl {
}
};
+ const _fetchQueryResult = (paragraph, clearChart, res) => {
+ _processQueryResult(paragraph, clearChart, res);
+ _tryStartRefresh(paragraph);
+ };
+
const _closeOldQuery = (paragraph) => {
const nid = paragraph.resNodeId;
@@ -1393,25 +1477,27 @@ export class NotebookCtrl {
return $q.when();
};
- $scope.cancelQuery = (paragraph) => {
- if (paragraph.subscription) {
- paragraph.subscription.unsubscribe();
- delete paragraph.subscription;
+ const _closeOldExport = (paragraph) => {
+ const nid = paragraph.exportNodeId;
+
+ if (paragraph.exportId) {
+ const exportId = paragraph.exportId;
+ delete paragraph.exportId;
+
+ return agentMgr.queryClose(nid, exportId);
}
- _showLoading(paragraph, false);
+ return $q.when();
+ };
+
+ $scope.cancelQuery = (paragraph) => {
+ paragraph.cancelQuerySubject.next(true);
+
this.$scope.stopRefresh(paragraph);
_closeOldQuery(paragraph)
- .then(() => _showLoading(paragraph, false))
- .catch((err) => {
- _showLoading(paragraph, false);
- paragraph.setError(err);
- });
- };
-
- $scope.cancelQueryAvailable = (paragraph) => {
- return !!paragraph.subscription;
+ .catch((err) => paragraph.setError(err))
+ .finally(() => paragraph.showLoading(false));
};
/**
@@ -1464,10 +1550,11 @@ export class NotebookCtrl {
const _executeRefresh = (paragraph) => {
const args = paragraph.queryArgs;
- agentMgr.awaitCluster()
- .then(() => _closeOldQuery(paragraph))
- .then(() => args.localNid || _chooseNode(args.cacheName,
false))
- .then((nid) => {
+ 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,
@@ -1481,16 +1568,24 @@ export class NotebookCtrl {
collocated: args.collocated
};
- return agentMgr.querySql(qryArg)
- .then((res) => _fetchQueryResult(paragraph, false,
res, qryArg));
- })
- .catch((err) => paragraph.setError(err));
+ 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) {
- _tryStopRefresh(paragraph);
-
- if (_.get(paragraph, 'rate.installed') &&
paragraph.queryExecuted()) {
+ if (_.get(paragraph, 'rate.installed') &&
paragraph.queryExecuted() && paragraph.nonRefresh()) {
$scope.chartAcceptKeyColumn(paragraph, TIME_LINE);
const delay = paragraph.rate.value * paragraph.rate.unit;
@@ -1532,23 +1627,34 @@ export class NotebookCtrl {
paragraph.resNodeId = res.responseNodeId;
paragraph.queryId = res.queryId;
- paragraph.rows = [];
- paragraph.gridOptions.adjustHeight(paragraph.rows.length);
+ if (paragraph.nonRefresh()) {
+ paragraph.rows = [];
- paragraph.meta = [];
- paragraph.setError({message: ''});
+ 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;
- $scope.queryAvailable(paragraph) &&
_chooseNode(paragraph.cacheName, local)
- .then((nid) => {
+ _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);
@@ -1556,66 +1662,61 @@ export class NotebookCtrl {
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
- };
+ paragraph.showLoading(true);
- 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
- };
+ const query = paragraph.partialQuery || paragraph.query;
- return agentMgr.querySql(qryArg)
- .then((res) => {
- _initQueryResult(paragraph, res);
+ const args = paragraph.queryArgs = {
+ cacheName: $scope.cacheNameForSql(paragraph),
+ query,
+ pageSize: paragraph.pageSize,
+ maxPages: paragraph.maxPages,
+ nonCollocatedJoins,
+ enforceJoinOrder,
+ localNid: local ? nid : null,
+ lazy,
+ collocated
+ };
- return _fetchQueryResult(paragraph, true,
res, qryArg);
- });
- })
- .catch((err) => {
- paragraph.setError(err);
+ ActivitiesData.post({ group: 'sql', action:
'/queries/execute' });
- _showLoading(paragraph, false);
+ 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);
- })
- .then(() => paragraph.ace.focus());
- });
+ }
+ );
+ }),
+ finalize(() => paragraph.showLoading(false))
+ ).toPromise();
};
const _cancelRefresh = (paragraph) => {
if (paragraph.rate && paragraph.rate.stopTime) {
delete paragraph.queryArgs;
- paragraph.rate.installed = false;
+ _.set(paragraph, 'rate.installed', false);
$interval.cancel(paragraph.rate.stopTime);
@@ -1624,25 +1725,23 @@ export class NotebookCtrl {
};
$scope.explain = (paragraph) => {
+ if (!$scope.queryAvailable(paragraph))
+ return;
+
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);
+ paragraph.showLoading(true);
- _closeOldQuery(paragraph)
- .then(() => _chooseNode(paragraph.cacheName, false))
- .then((nid) => {
+ from(_chooseNode(paragraph.cacheName, false)).pipe(
+ switchMap((nid) => {
const args = paragraph.queryArgs = {
- type: 'EXPLAIN',
cacheName: $scope.cacheNameForSql(paragraph),
query: 'EXPLAIN ' + (paragraph.partialQuery ||
paragraph.query),
pageSize: paragraph.pageSize
@@ -1662,76 +1761,64 @@ export class NotebookCtrl {
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());
+ 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;
- $scope.scanAvailable(paragraph) && _chooseNode(cacheName, local)
- .then((nid) => {
+ from(_chooseNode(cacheName, local)).pipe(
+ switchMap((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
- };
+ paragraph.showLoading(true);
- ActivitiesData.post({ group: 'sql', action:
'/queries/scan' });
-
- const qryArg = {
- nid,
- cacheName,
- filter,
- regEx: false,
- caseSensitive,
- near: false,
- local,
- pageSize
- };
+ const qryArg = paragraph.queryArgs = {
+ cacheName,
+ filter,
+ regEx: false,
+ caseSensitive,
+ near: false,
+ pageSize,
+ localNid: local ? nid : null
+ };
- return agentMgr.queryScan(qryArg).then((res) => {
- _initQueryResult(paragraph, res);
+ qryArg.nid = nid;
+ qryArg.local = local;
- return _fetchQueryResult(paragraph, true, res,
qryArg);
- });
- })
- .catch((err) => {
- paragraph.setError(err);
+ ActivitiesData.post({ group: 'sql', action:
'/queries/scan' });
- _showLoading(paragraph, false);
- });
- });
+ 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) {
@@ -1751,41 +1838,41 @@ export class NotebookCtrl {
});
}
- $scope.nextPage = (paragraph) => {
- _showLoading(paragraph, true);
+ const _processQueryNextPage = (paragraph, res) => {
+ paragraph.page++;
- paragraph.queryArgs.pageSize = paragraph.pageSize;
+ paragraph.total += paragraph.rows.length;
- agentMgr.queryNextPage(paragraph.resNodeId, paragraph.queryId,
paragraph.pageSize)
- .then((res) => {
- paragraph.page++;
+ paragraph.duration = res.duration;
- paragraph.total += paragraph.rows.length;
+ paragraph.rows = res.rows;
- paragraph.duration = res.duration;
+ if (paragraph.chart()) {
+ if (paragraph.result === 'pie')
+ _updatePieChartsWithData(paragraph,
_pieChartDatum(paragraph));
+ else
+ _updateChartsWithData(paragraph, _chartDatum(paragraph));
+ }
- paragraph.rows = res.rows;
+ paragraph.gridOptions.adjustHeight(paragraph.rows.length);
- if (paragraph.chart()) {
- if (paragraph.result === 'pie')
- _updatePieChartsWithData(paragraph,
_pieChartDatum(paragraph));
- else
- _updateChartsWithData(paragraph,
_chartDatum(paragraph));
- }
+ paragraph.showLoading(false);
- paragraph.gridOptions.adjustHeight(paragraph.rows.length);
+ if (!res.hasMore)
+ delete paragraph.queryId;
+ };
- _showLoading(paragraph, false);
+ $scope.nextPage = (paragraph) => {
+ paragraph.showLoading(true);
- if (!res.hasMore)
- delete paragraph.queryId;
- })
+ paragraph.queryArgs.pageSize = paragraph.pageSize;
+
+ agentMgr.queryNextPage(paragraph.resNodeId, paragraph.queryId,
paragraph.pageSize)
+ .then((res) => _processQueryNextPage(paragraph, res))
.catch((err) => {
paragraph.setError(err);
-
- _showLoading(paragraph, false);
- })
- .then(() => paragraph.ace && paragraph.ace.focus());
+ paragraph.ace && paragraph.ace.focus();
+ });
};
const _export = (fileName, columnDefs, meta, rows, toClipBoard =
false) => {
@@ -1844,7 +1931,7 @@ export class NotebookCtrl {
const exportFileName = (paragraph, all) => {
const args = paragraph.queryArgs;
- if (args.type === 'SCAN')
+ if (paragraph.qryType === 'scan')
return `export-scan-${args.cacheName}-${paragraph.name}${all ?
'-all' : ''}.csv`;
return `export-query-${paragraph.name}${all ? '-all' : ''}.csv`;
@@ -1865,39 +1952,26 @@ export class NotebookCtrl {
};
$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;
+ paragraph.cancelExportSubject.next(true);
- return paragraph.ace && paragraph.ace.focus();
- });
+ 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) {
@@ -1924,6 +1998,8 @@ export class NotebookCtrl {
};
$scope.startRefresh = function(paragraph, value, unit) {
+ $scope.stopRefresh(paragraph);
+
paragraph.rate.value = value;
paragraph.rate.unit = unit;
paragraph.rate.installed = true;
@@ -1933,7 +2009,7 @@ export class NotebookCtrl {
};
$scope.stopRefresh = function(paragraph) {
- paragraph.rate.installed = false;
+ _.set(paragraph, 'rate.installed', false);
_tryStopRefresh(paragraph);
};
@@ -2022,7 +2098,7 @@ export class NotebookCtrl {
if (!_.isNil(paragraph)) {
const scope = $scope.$new();
- if (paragraph.queryArgs.type === 'SCAN') {
+ if (paragraph.qryType === 'scan') {
scope.title = 'SCAN query';
const filter = paragraph.queryArgs.filter;
@@ -2032,7 +2108,7 @@ export class NotebookCtrl {
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 ')) {
+ else if (paragraph.queryArgs.query.startsWith('EXPLAIN ')) {
scope.title = 'Explain query';
scope.content = paragraph.queryArgs.query.split(/\r?\n/);
}
@@ -2076,21 +2152,77 @@ export class NotebookCtrl {
}
};
- $window.addEventListener('beforeunload', () => {
- this._closeOpenedQueries(this.$scope.notebook.paragraphs);
+ 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) {
- _.forEach(paragraphs, ({queryId, subscription, resNodeId}) => {
- if (subscription)
- subscription.unsubscribe();
+ 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);
- if (queryId) {
- this.agentMgr.queryClose(resNodeId, queryId)
- .catch(() => { /* No-op. */ });
+ return true;
}
- });
+ catch (ignored) {
+ return false;
+ }
+ }
+
+ return true;
}
scanActions: QueryActions<Paragraph & {type: 'scan'}> = [
@@ -2148,9 +2280,13 @@ export class NotebookCtrl {
async removeParagraph(paragraph: Paragraph) {
try {
- await this.Confirm.confirm('Are you sure you want to remove query:
"' + paragraph.name + '"?');
- this.$scope.stopRefresh(paragraph);
+ 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);
@@ -2162,6 +2298,9 @@ export class NotebookCtrl {
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);
}
@@ -2184,9 +2323,13 @@ export class NotebookCtrl {
}
$onDestroy() {
- this._closeOpenedQueries(this.$scope.notebook.paragraphs);
-
if (this.subscribers$)
this.subscribers$.unsubscribe();
+
+ if (this.offTransitions)
+ this.offTransitions();
+
+ this.agentMgr.removeClusterSwitchListener(this.onClusterSwitchLnr);
+ this.$window.removeEventListener('beforeunload',
this.closeOpenedQueries);
}
}
diff --git
a/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/template.tpl.pug
b/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/template.tpl.pug
index 5342333..ecedaf9 100644
---
a/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/template.tpl.pug
+++
b/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/template.tpl.pug
@@ -182,7 +182,7 @@ mixin query-actions
button.btn-ignite.btn-ignite--secondary(ng-disabled='!queryAvailable(paragraph)'
ng-click='explain(paragraph)' data-placement='bottom' bs-tooltip=''
data-title='{{queryTooltip(paragraph, "explain query")}}')
| Explain
-
button.btn-ignite.btn-ignite--secondary(ng-if='cancelQueryAvailable(paragraph)'
ng-click='cancelQuery(paragraph)' data-placement='bottom' bs-tooltip=''
data-title='{{"Cancel query execution"}}')
+
button.btn-ignite.btn-ignite--secondary(ng-if='paragraph.executionInProgress(false)
|| paragraph.executionInProgress(true)' ng-click='cancelQuery(paragraph)'
data-placement='bottom' bs-tooltip='' data-title='{{"Cancel query execution"}}')
| Cancel
mixin table-result-heading-query
@@ -338,7 +338,7 @@ mixin paragraph-scan
span.icon-left.fa.fa-fw.fa-refresh.fa-spin(ng-show='paragraph.checkScanInProgress(true)')
| Scan on selected node
-
button.btn-ignite.btn-ignite--secondary(ng-if='cancelQueryAvailable(paragraph)'
ng-click='cancelQuery(paragraph)' data-placement='bottom' bs-tooltip=''
data-title='{{"Cancel query execution"}}')
+
button.btn-ignite.btn-ignite--secondary(ng-if='paragraph.checkScanInProgress(false)
|| paragraph.checkScanInProgress(true)' ng-click='cancelQuery(paragraph)'
data-placement='bottom' bs-tooltip='' data-title='{{"Cancel query execution"}}')
| Cancel
div
diff --git
a/modules/web-console/frontend/app/modules/agent/AgentManager.service.js
b/modules/web-console/frontend/app/modules/agent/AgentManager.service.js
index 0c698c1..c476578 100644
--- a/modules/web-console/frontend/app/modules/agent/AgentManager.service.js
+++ b/modules/web-console/frontend/app/modules/agent/AgentManager.service.js
@@ -45,6 +45,11 @@ const LAZY_QUERY_SINCE = [['2.1.4-p1', '2.2.0'], '2.2.1'];
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 COLLECT_BY_CACHE_GROUPS_SINCE = '2.7.0';
+/**
+ * Query execution result.
+ * @typedef {{responseNodeId: String, queryId: String, columns: String[],
rows: {Object[][]}, hasMore: Boolean, duration: Number}} VisorQueryResult
+ */
+
/** Reserved cache names */
const RESERVED_CACHE_NAMES = [
'ignite-hadoop-mr-sys-cache',
@@ -150,6 +155,17 @@ export default class AgentManager {
socket = null;
+ /** @type {Set<() => Promise>} */
+ switchClusterListeners = new Set();
+
+ addClusterSwitchListener(func) {
+ this.switchClusterListeners.add(func);
+ }
+
+ removeClusterSwitchListener(func) {
+ this.switchClusterListeners.delete(func);
+ }
+
static restoreActiveCluster() {
try {
return JSON.parse(localStorage.cluster);
@@ -280,13 +296,18 @@ export default class AgentManager {
}
switchCluster(cluster) {
- const state = this.connectionSbj.getValue();
+ return Promise.all(_.map([...this.switchClusterListeners], (lnr) =>
lnr()))
+ .then(() => {
+ const state = this.connectionSbj.getValue();
+
+ state.updateCluster(cluster);
- state.updateCluster(cluster);
+ this.connectionSbj.next(state);
- this.connectionSbj.next(state);
+ this.saveToStorage(cluster);
- this.saveToStorage(cluster);
+ return Promise.resolve();
+ });
}
/**
@@ -678,7 +699,7 @@ export default class AgentManager {
* @param {Number} pageSize
* @param {Boolean} [lazy] query flag.
* @param {Boolean} [collocated] Collocated query.
- * @returns {Promise}
+ * @returns {Promise.<VisorQueryResult>} Query execution result.
*/
querySql({nid, cacheName, query, nonCollocatedJoins, enforceJoinOrder,
replicatedOnly, local, pageSize, lazy = false, collocated = false}) {
if (this.available(IGNITE_2_0)) {
@@ -721,7 +742,7 @@ export default class AgentManager {
* @param {String} nid Node id.
* @param {String} queryId Query ID.
* @param {Number} pageSize
- * @returns {Promise}
+ * @returns {Promise.<VisorQueryResult>} Query execution result.
*/
queryFetchFistsPage(nid, queryId, pageSize) {
return this.visorTask('queryFetchFirstPage', nid, queryId,
pageSize).then(({error, result}) => {
@@ -736,7 +757,7 @@ export default class AgentManager {
* @param {String} nid Node id.
* @param {Number} queryId
* @param {Number} pageSize
- * @returns {Promise}
+ * @returns {Promise.<VisorQueryResult>} Query execution result.
*/
queryNextPage(nid, queryId, pageSize) {
if (this.available(IGNITE_2_0))
@@ -745,58 +766,10 @@ export default class AgentManager {
return this.visorTask('queryFetch', nid, queryId, pageSize);
}
- _fetchQueryAllResults(acc, pageSize) {
- if (!_.isNil(acc.rows)) {
- if (!acc.hasMore)
- return acc;
-
- return of(acc).pipe(
- expand((acc) => {
- return from(this.queryNextPage(acc.responseNodeId,
acc.queryId, pageSize)
- .then((res) => {
- acc.rows = acc.rows.concat(res.rows);
- acc.hasMore = res.hasMore;
-
- return acc;
- }));
- }),
- takeWhile((acc) => acc.hasMore),
- last()
- ).toPromise();
- }
-
- return timer(100, 500).pipe(
- exhaustMap(() => this.queryFetchFistsPage(acc.responseNodeId,
acc.queryId, pageSize)),
- filter((res) => !_.isNil(res.rows)),
- first(),
- map((res) => this._fetchQueryAllResults(res, 1024))
- ).toPromise();
- }
-
- /**
- * @param {String} nid Node id.
- * @param {String} cacheName Cache name.
- * @param {String} [query] Query if null then scan query.
- * @param {Boolean} nonCollocatedJoins Flag whether to execute non
collocated joins.
- * @param {Boolean} enforceJoinOrder Flag whether enforce join order is
enabled.
- * @param {Boolean} replicatedOnly Flag whether query contains only
replicated tables.
- * @param {Boolean} local Flag whether to execute query locally.
- * @param {Boolean} lazy query flag.
- * @param {Boolean} collocated Collocated query.
- * @returns {Promise}
- */
- querySqlGetAll({nid, cacheName, query, nonCollocatedJoins,
enforceJoinOrder, replicatedOnly, local, lazy, collocated}) {
- // Page size for query.
- const pageSize = 1024;
-
- return this.querySql({nid, cacheName, query, nonCollocatedJoins,
enforceJoinOrder, replicatedOnly, local, pageSize, lazy, collocated})
- .then((res) => this._fetchQueryAllResults(res, pageSize));
- }
-
/**
* @param {String} nid Node id.
* @param {Number} [queryId]
- * @returns {Promise}
+ * @returns {Promise<Void>}
*/
queryClose(nid, queryId) {
if (this.available(IGNITE_2_0)) {
@@ -816,7 +789,7 @@ export default class AgentManager {
* @param {Boolean} near Scan near cache.
* @param {Boolean} local Flag whether to execute query locally.
* @param {Number} pageSize Page size.
- * @returns {Promise}
+ * @returns {Promise.<VisorQueryResult>} Query execution result.
*/
queryScan({nid, cacheName, filter, regEx, caseSensitive, near, local,
pageSize}) {
if (this.available(IGNITE_2_0)) {
@@ -842,24 +815,6 @@ export default class AgentManager {
}
/**
- * @param {String} nid Node id.
- * @param {String} cacheName Cache name.
- * @param {String} filter Filter text.
- * @param {Boolean} regEx Flag whether filter by regexp.
- * @param {Boolean} caseSensitive Case sensitive filtration.
- * @param {Boolean} near Scan near cache.
- * @param {Boolean} local Flag whether to execute query locally.
- * @returns {Promise}
- */
- queryScanGetAll({nid, cacheName, filter, regEx, caseSensitive, near,
local}) {
- // Page size for query.
- const pageSize = 1024;
-
- return this.queryScan({nid, cacheName, filter, regEx, caseSensitive,
near, local, pageSize})
- .then((res) => this._fetchQueryAllResults(res, pageSize));
- }
-
- /**
* Change cluster active state.
*
* @returns {Promise}