diff --git a/superset/assets/cypress/integration/dashboard/controls.js b/superset/assets/cypress/integration/dashboard/controls.js
index cfde066a1f2..b79abf8bada 100644
--- a/superset/assets/cypress/integration/dashboard/controls.js
+++ b/superset/assets/cypress/integration/dashboard/controls.js
@@ -1,4 +1,5 @@
import { WORLD_HEALTH_DASHBOARD, CHECK_DASHBOARD_FAVORITE_ENDPOINT } from './dashboard.helper';
+import readResponseBlob from '../../utils/readResponseBlob';
export default () => describe('top-level controls', () => {
let sliceIds = [];
@@ -61,8 +62,9 @@ export default () => describe('top-level controls', () => {
cy.wait(forceRefreshRequests).then((xhrs) => {
// is_cached in response should be false
- xhrs.forEach((xhr) => {
- expect(xhr.response.body.is_cached).to.equal(false);
+ xhrs.forEach(async (xhr) => {
+ const responseBody = await readResponseBlob(xhr.response.body);
+ expect(responseBody.is_cached).to.equal(false);
});
});
});
diff --git a/superset/assets/cypress/integration/dashboard/filter.js b/superset/assets/cypress/integration/dashboard/filter.js
index fe236db5cb7..e0e65d889ae 100644
--- a/superset/assets/cypress/integration/dashboard/filter.js
+++ b/superset/assets/cypress/integration/dashboard/filter.js
@@ -40,11 +40,11 @@ export default () => describe('dashboard filter', () => {
.type('South Asia{enter}', { force: true });
cy.wait(aliases).then((requests) => {
- requests.forEach((request) => {
- const requestBody = request.request.body.substring('form_data='.length);
- const requestParams = JSON.parse(decodeURIComponent(requestBody));
+ requests.forEach((xhr) => {
+ const requestFormData = xhr.request.body;
+ const requestParams = JSON.parse(requestFormData.get('form_data'));
expect(requestParams.extra_filters[0])
- .deep.eq({ col: 'region', op: 'in', val: ['South+Asia'] });
+ .deep.eq({ col: 'region', op: 'in', val: ['South Asia'] });
});
});
});
diff --git a/superset/assets/cypress/integration/dashboard/load.js b/superset/assets/cypress/integration/dashboard/load.js
index 4da064bbb0a..834241153be 100644
--- a/superset/assets/cypress/integration/dashboard/load.js
+++ b/superset/assets/cypress/integration/dashboard/load.js
@@ -1,3 +1,4 @@
+import readResponseBlob from '../../utils/readResponseBlob';
import { WORLD_HEALTH_DASHBOARD } from './dashboard.helper';
export default () => describe('load', () => {
@@ -24,9 +25,10 @@ export default () => describe('load', () => {
it('should load dashboard', () => {
// wait and verify one-by-one
cy.wait(aliases).then((requests) => {
- requests.forEach((xhr) => {
+ requests.forEach(async (xhr) => {
expect(xhr.status).to.eq(200);
- expect(xhr.response.body).to.have.property('error', null);
+ const responseBody = await readResponseBlob(xhr.response.body);
+ expect(responseBody).to.have.property('error', null);
cy.get(`#slice-container-${xhr.response.body.form_data.slice_id}`);
});
});
diff --git a/superset/assets/cypress/integration/explore/visualizations/big_number_total.js b/superset/assets/cypress/integration/explore/visualizations/big_number_total.js
index 1797df62b7d..b9cd13b5848 100644
--- a/superset/assets/cypress/integration/explore/visualizations/big_number_total.js
+++ b/superset/assets/cypress/integration/explore/visualizations/big_number_total.js
@@ -1,4 +1,5 @@
import { FORM_DATA_DEFAULTS, NUM_METRIC } from './shared.helper';
+import readResponseBlob from '../../../utils/readResponseBlob';
// Big Number Total
@@ -42,10 +43,12 @@ export default () => describe('Big Number Total', () => {
const formData = { ...BIG_NUMBER_DEFAULTS, metric: NUM_METRIC, groupby: ['state'] };
cy.visitChartByParams(JSON.stringify(formData));
- cy.wait(['@getJson']).then((data) => {
- cy.verifyResponseCodes(data);
+ cy.wait(['@getJson']).then(async (xhr) => {
+ cy.verifyResponseCodes(xhr);
cy.verifySliceContainer();
- expect(data.response.body.query).not.contains(formData.groupby[0]);
+
+ const responseBody = await readResponseBlob(xhr.response.body);
+ expect(responseBody.query).not.contains(formData.groupby[0]);
});
});
});
diff --git a/superset/assets/cypress/integration/explore/visualizations/table.js b/superset/assets/cypress/integration/explore/visualizations/table.js
index 92aabb63a00..0f701063767 100644
--- a/superset/assets/cypress/integration/explore/visualizations/table.js
+++ b/superset/assets/cypress/integration/explore/visualizations/table.js
@@ -1,4 +1,5 @@
import { FORM_DATA_DEFAULTS, NUM_METRIC, SIMPLE_FILTER } from './shared.helper';
+import readResponseBlob from '../../../utils/readResponseBlob';
// Table
@@ -59,10 +60,11 @@ export default () => describe('Table chart', () => {
cy.visitChartByParams(JSON.stringify(formData));
- cy.wait('@getJson').then((data) => {
- cy.verifyResponseCodes(data);
+ cy.wait('@getJson').then(async (xhr) => {
+ cy.verifyResponseCodes(xhr);
cy.verifySliceContainer('table');
- expect(data.response.body.data.records.length).to.eq(limit);
+ const responseBody = await readResponseBlob(xhr.response.body);
+ expect(responseBody.data.records.length).to.eq(limit);
});
});
@@ -85,10 +87,11 @@ export default () => describe('Table chart', () => {
};
cy.visitChartByParams(JSON.stringify(formData));
- cy.wait('@getJson').then((data) => {
- cy.verifyResponseCodes(data);
+ cy.wait('@getJson').then(async (xhr) => {
+ cy.verifyResponseCodes(xhr);
cy.verifySliceContainer('table');
- const records = data.response.body.data.records;
+ const responseBody = await readResponseBlob(xhr.response.body);
+ const { records } = responseBody.data;
expect(records[0].num).greaterThan(records[records.length - 1].num);
});
});
diff --git a/superset/assets/cypress/support/commands.js b/superset/assets/cypress/support/commands.js
index a8d432e1b63..4fbadef68f7 100644
--- a/superset/assets/cypress/support/commands.js
+++ b/superset/assets/cypress/support/commands.js
@@ -24,6 +24,8 @@
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
+import readResponseBlob from '../utils/readResponseBlob';
+
const BASE_EXPLORE_URL = '/superset/explore/?form_data=';
Cypress.Commands.add('login', () => {
@@ -50,11 +52,14 @@ Cypress.Commands.add('visitChartByParams', (params) => {
cy.visit(`${BASE_EXPLORE_URL}${params}`);
});
-Cypress.Commands.add('verifyResponseCodes', (data) => {
+Cypress.Commands.add('verifyResponseCodes', async (xhr) => {
// After a wait response check for valid response
- expect(data.status).to.eq(200);
- if (data.response.body.error) {
- expect(data.response.body.error).to.eq(null);
+ expect(xhr.status).to.eq(200);
+
+ const responseBody = await readResponseBlob(xhr.response.body);
+
+ if (responseBody.error) {
+ expect(responseBody.error).to.eq(null);
}
});
@@ -72,11 +77,12 @@ Cypress.Commands.add('verifySliceContainer', (chartSelector) => {
});
Cypress.Commands.add('verifySliceSuccess', ({ waitAlias, querySubstring, chartSelector }) => {
- cy.wait(waitAlias).then((data) => {
- cy.verifyResponseCodes(data);
+ cy.wait(waitAlias).then(async (xhr) => {
+ cy.verifyResponseCodes(xhr);
+ const responseBody = await readResponseBlob(xhr.response.body);
if (querySubstring) {
- expect(data.response.body.query).contains(querySubstring);
+ expect(responseBody.query).contains(querySubstring);
}
cy.verifySliceContainer(chartSelector);
diff --git a/superset/assets/cypress/support/index.js b/superset/assets/cypress/support/index.js
index 37a498fb5bf..8b273dfb6fc 100644
--- a/superset/assets/cypress/support/index.js
+++ b/superset/assets/cypress/support/index.js
@@ -13,8 +13,11 @@
// https://on.cypress.io/configuration
// ***********************************************************
-// Import commands.js using ES2015 syntax:
import './commands';
-// Alternatively you can use CommonJS syntax:
-// require('./commands')
+// The following is a workaround for Cypress not supporting fetch.
+// By setting window.fetch = null, we force the fetch polyfill to fall back
+// to xhr as described here https://github.com/cypress-io/cypress/issues/95
+Cypress.on('window:before:load', (win) => {
+ win.fetch = null; // eslint-disable-line no-param-reassign
+});
diff --git a/superset/assets/cypress/utils/readResponseBlob.js b/superset/assets/cypress/utils/readResponseBlob.js
new file mode 100644
index 00000000000..bcbe13736d0
--- /dev/null
+++ b/superset/assets/cypress/utils/readResponseBlob.js
@@ -0,0 +1,11 @@
+// This function returns a promise that resolves to the value
+// of the passed response blob. It assumes the blob should be read as text,
+// and that the response can be parsed as JSON. This is needed to read
+// the value of any fetch-based response.
+export default function readResponseBlob(blob) {
+ return new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(JSON.parse(reader.result));
+ reader.readAsText(blob);
+ });
+}
diff --git a/superset/assets/package.json b/superset/assets/package.json
index df297e4f993..52c62156280 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -51,9 +51,11 @@
"@data-ui/sparkline": "^0.0.54",
"@data-ui/theme": "^0.0.62",
"@data-ui/xy-chart": "^0.0.61",
+ "@superset-ui/core": "^0.0.5",
"@vx/legend": "^0.0.170",
"@vx/responsive": "0.0.172",
"@vx/scale": "^0.0.165",
+ "abortcontroller-polyfill": "^1.1.9",
"babel-register": "^6.24.1",
"bootstrap": "^3.3.6",
"bootstrap-slider": "^10.0.0",
@@ -158,6 +160,7 @@
"eslint-plugin-prettier": "^2.6.0",
"eslint-plugin-react": "^7.0.1",
"exports-loader": "^0.7.0",
+ "fetch-mock": "^7.0.0-alpha.6",
"file-loader": "^1.1.11",
"gl": "^4.0.4",
"ignore-styles": "^5.0.1",
diff --git a/superset/assets/spec/helpers/setupSupersetClient.js b/superset/assets/spec/helpers/setupSupersetClient.js
new file mode 100644
index 00000000000..dd6367cbc2e
--- /dev/null
+++ b/superset/assets/spec/helpers/setupSupersetClient.js
@@ -0,0 +1,10 @@
+import fetchMock from 'fetch-mock';
+import { SupersetClient } from '@superset-ui/core';
+
+export default function setupSupersetClient() {
+ // The following is needed to mock out SupersetClient requests
+ // including CSRF authentication and initialization
+ global.FormData = window.FormData; // used by SupersetClient
+ fetchMock.get('glob:*superset/csrf_token/*', { csrf_token: '1234' });
+ SupersetClient.configure({ protocol: 'http', host: 'localhost' }).init();
+}
diff --git a/superset/assets/spec/helpers/shim.js b/superset/assets/spec/helpers/shim.js
index 306d35a233e..e63ea984aee 100644
--- a/superset/assets/spec/helpers/shim.js
+++ b/superset/assets/spec/helpers/shim.js
@@ -1,5 +1,6 @@
/* eslint no-native-reassign: 0 */
import 'babel-polyfill';
+import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
import jsdom from 'jsdom';
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
diff --git a/superset/assets/spec/javascripts/explore/chartActions_spec.js b/superset/assets/spec/javascripts/explore/chartActions_spec.js
index e3a621b3c38..ecac2e2ef6b 100644
--- a/superset/assets/spec/javascripts/explore/chartActions_spec.js
+++ b/superset/assets/spec/javascripts/explore/chartActions_spec.js
@@ -1,37 +1,123 @@
+import fetchMock from 'fetch-mock';
import sinon from 'sinon';
-import $ from 'jquery';
+
+import { Logger } from '../../../src/logger';
+import setupSupersetClient from '../../helpers/setupSupersetClient';
import * as exploreUtils from '../../../src/explore/exploreUtils';
import * as actions from '../../../src/chart/chartAction';
describe('chart actions', () => {
+ const MOCK_URL = '/mockURL';
let dispatch;
let urlStub;
- let ajaxStub;
- let request;
+ let loggerStub;
+
+ const setupDefaultFetchMock = () => {
+ fetchMock.post(MOCK_URL, { json: {} }, { overwriteRoutes: true });
+ };
+
+ beforeAll(() => {
+ setupSupersetClient();
+ setupDefaultFetchMock();
+ });
+
+ afterAll(fetchMock.restore);
beforeEach(() => {
dispatch = sinon.spy();
- urlStub = sinon.stub(exploreUtils, 'getExploreUrlAndPayload')
- .callsFake(() => ({ url: 'mockURL', payload: {} }));
- ajaxStub = sinon.stub($, 'ajax');
+ urlStub = sinon
+ .stub(exploreUtils, 'getExploreUrlAndPayload')
+ .callsFake(() => ({ url: MOCK_URL, payload: {} }));
+ loggerStub = sinon.stub(Logger, 'append');
});
afterEach(() => {
urlStub.restore();
- ajaxStub.restore();
+ loggerStub.restore();
+ fetchMock.resetHistory();
});
- it('should handle query timeout', () => {
- ajaxStub.rejects({ statusText: 'timeout' });
- request = actions.runQuery({});
- const promise = request(dispatch, sinon.stub().returns({
- explore: {
- controls: [],
- },
- }));
- promise.then(() => {
- expect(dispatch.callCount).toBe(3);
- expect(dispatch.args[0][0].type).toBe(actions.CHART_UPDATE_TIMEOUT);
+ it('should dispatch CHART_UPDATE_STARTED action before the query', () => {
+ const actionThunk = actions.runQuery({});
+
+ return actionThunk(dispatch).then(() => {
+ // chart update, trigger query, update form data, success
+ expect(dispatch.callCount).toBe(4);
+ expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
+ expect(dispatch.args[0][0].type).toBe(actions.CHART_UPDATE_STARTED);
+
+ return Promise.resolve();
+ });
+ });
+
+ it('should dispatch TRIGGER_QUERY action with the query', () => {
+ const actionThunk = actions.runQuery({});
+ return actionThunk(dispatch).then(() => {
+ // chart update, trigger query, update form data, success
+ expect(dispatch.callCount).toBe(4);
+ expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
+ expect(dispatch.args[1][0].type).toBe(actions.TRIGGER_QUERY);
+
+ return Promise.resolve();
+ });
+ });
+
+ it('should dispatch UPDATE_QUERY_FORM_DATA action with the query', () => {
+ const actionThunk = actions.runQuery({});
+ return actionThunk(dispatch).then(() => {
+ // chart update, trigger query, update form data, success
+ expect(dispatch.callCount).toBe(4);
+ expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
+ expect(dispatch.args[2][0].type).toBe(actions.UPDATE_QUERY_FORM_DATA);
+
+ return Promise.resolve();
+ });
+ });
+
+ it('should dispatch CHART_UPDATE_SUCCEEDED action upon success', () => {
+ const actionThunk = actions.runQuery({});
+ return actionThunk(dispatch).then(() => {
+ // chart update, trigger query, update form data, success
+ expect(dispatch.callCount).toBe(4);
+ expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
+ expect(dispatch.args[3][0].type).toBe(actions.CHART_UPDATE_SUCCEEDED);
+
+ return Promise.resolve();
+ });
+ });
+
+ it('should CHART_UPDATE_TIMEOUT action upon query timeout', () => {
+ const unresolvingPromise = new Promise(() => {});
+ fetchMock.post(MOCK_URL, () => unresolvingPromise, { overwriteRoutes: true });
+
+ const timeoutInSec = 1 / 1000;
+ const actionThunk = actions.runQuery({}, false, timeoutInSec);
+
+ return actionThunk(dispatch).then(() => {
+ // chart update, trigger query, update form data, fail
+ expect(dispatch.callCount).toBe(4);
+ expect(dispatch.args[3][0].type).toBe(actions.CHART_UPDATE_TIMEOUT);
+ setupDefaultFetchMock();
+
+ return Promise.resolve();
+ });
+ });
+
+ it('should dispatch CHART_UPDATE_FAILED action upon non-timeout non-abort failure', () => {
+ fetchMock.post(MOCK_URL, { throws: { error: 'misc error' } }, { overwriteRoutes: true });
+
+ const timeoutInSec = 1 / 1000;
+ const actionThunk = actions.runQuery({}, false, timeoutInSec);
+
+ return actionThunk(dispatch).then(() => {
+ // chart update, trigger query, update form data, fail
+ expect(dispatch.callCount).toBe(4);
+ const updateFailedAction = dispatch.args[3][0];
+ expect(updateFailedAction.type).toBe(actions.CHART_UPDATE_FAILED);
+ expect(updateFailedAction.queryResponse.error).toBe('misc error');
+ setupDefaultFetchMock();
+
+ return Promise.resolve();
});
});
});
diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx
index 0d550272d67..4944030a572 100644
--- a/superset/assets/src/chart/Chart.jsx
+++ b/superset/assets/src/chart/Chart.jsx
@@ -31,7 +31,6 @@ const propTypes = {
chartUpdateEndTime: PropTypes.number,
chartUpdateStartTime: PropTypes.number,
latestQueryFormData: PropTypes.object,
- queryRequest: PropTypes.object,
queryResponse: PropTypes.object,
lastRendered: PropTypes.number,
triggerQuery: PropTypes.bool,
diff --git a/superset/assets/src/chart/chartAction.js b/superset/assets/src/chart/chartAction.js
index 8adbdc0a566..12df6a2bc4b 100644
--- a/superset/assets/src/chart/chartAction.js
+++ b/superset/assets/src/chart/chartAction.js
@@ -1,16 +1,16 @@
-import URI from 'urijs';
-
+/* global window, AbortController */
+/* eslint no-undef: 'error' */
+import { SupersetClient } from '@superset-ui/core';
import { getExploreUrlAndPayload, getAnnotationJsonUrl } from '../explore/exploreUtils';
import { requiresQuery, ANNOTATION_SOURCE_TYPES } from '../modules/AnnotationTypes';
+import { addDangerToast } from '../messageToasts/actions';
import { Logger, LOG_ACTIONS_LOAD_CHART } from '../logger';
import { COMMON_ERR_MESSAGES } from '../utils/common';
import { t } from '../locales';
-const $ = (window.$ = require('jquery'));
-
export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
-export function chartUpdateStarted(queryRequest, latestQueryFormData, key) {
- return { type: CHART_UPDATE_STARTED, queryRequest, latestQueryFormData, key };
+export function chartUpdateStarted(queryController, latestQueryFormData, key) {
+ return { type: CHART_UPDATE_STARTED, queryController, latestQueryFormData, key };
}
export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
@@ -54,8 +54,8 @@ export function annotationQuerySuccess(annotation, queryResponse, key) {
}
export const ANNOTATION_QUERY_STARTED = 'ANNOTATION_QUERY_STARTED';
-export function annotationQueryStarted(annotation, queryRequest, key) {
- return { type: ANNOTATION_QUERY_STARTED, annotation, queryRequest, key };
+export function annotationQueryStarted(annotation, queryController, key) {
+ return { type: ANNOTATION_QUERY_STARTED, annotation, queryController, key };
}
export const ANNOTATION_QUERY_FAILED = 'ANNOTATION_QUERY_FAILED';
@@ -85,18 +85,21 @@ export function runAnnotationQuery(annotation, timeout = 60, formData = null, ke
);
const isNative = annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE;
const url = getAnnotationJsonUrl(annotation.value, sliceFormData, isNative);
- const queryRequest = $.ajax({
+ const controller = new AbortController();
+ const { signal } = controller;
+
+ dispatch(annotationQueryStarted(annotation, controller, sliceKey));
+
+ return SupersetClient.get({
url,
- dataType: 'json',
+ signal,
timeout: timeout * 1000,
- });
- dispatch(annotationQueryStarted(annotation, queryRequest, sliceKey));
- return queryRequest
- .then(queryResponse => dispatch(annotationQuerySuccess(annotation, queryResponse, sliceKey)))
+ })
+ .then(({ json }) => dispatch(annotationQuerySuccess(annotation, json, sliceKey)))
.catch((err) => {
if (err.statusText === 'timeout') {
dispatch(annotationQueryFailed(annotation, { error: 'Query Timeout' }, sliceKey));
- } else if ((err.responseJSON.error || '').toLowerCase().startsWith('no data')) {
+ } else if ((err.responseJSON.error || '').toLowerCase().includes('no data')) {
dispatch(annotationQuerySuccess(annotation, err, sliceKey));
} else if (err.statusText !== 'abort') {
dispatch(annotationQueryFailed(annotation, err.responseJSON, sliceKey));
@@ -135,30 +138,30 @@ export function runQuery(formData, force = false, timeout = 60, key) {
force,
});
const logStart = Logger.getTimestamp();
- const queryRequest = $.ajax({
- type: 'POST',
+ const controller = new AbortController();
+ const { signal } = controller;
+
+ dispatch(chartUpdateStarted(controller, payload, key));
+
+ const queryPromise = SupersetClient.post({
url,
- dataType: 'json',
- data: {
- form_data: JSON.stringify(payload),
- },
+ postPayload: { form_data: payload },
+ signal,
timeout: timeout * 1000,
- });
- const queryPromise = Promise.resolve(dispatch(chartUpdateStarted(queryRequest, payload, key)))
- .then(() => queryRequest)
- .then((queryResponse) => {
+ })
+ .then(({ json }) => {
Logger.append(LOG_ACTIONS_LOAD_CHART, {
slice_id: key,
- is_cached: queryResponse.is_cached,
+ is_cached: json.is_cached,
force_refresh: force,
- row_count: queryResponse.rowcount,
+ row_count: json.rowcount,
datasource: formData.datasource,
start_offset: logStart,
duration: Logger.getTimestamp() - logStart,
has_extra_filters: formData.extra_filters && formData.extra_filters.length > 0,
viz_type: formData.viz_type,
});
- return dispatch(chartUpdateSucceeded(queryResponse, key));
+ return dispatch(chartUpdateSucceeded(json, key));
})
.catch((err) => {
Logger.append(LOG_ACTIONS_LOAD_CHART, {
@@ -170,30 +173,30 @@ export function runQuery(formData, force = false, timeout = 60, key) {
});
if (err.statusText === 'timeout') {
dispatch(chartUpdateTimeout(err.statusText, timeout, key));
- } else if (err.statusText === 'abort') {
+ } else if (err.statusText === 'AbortError') {
dispatch(chartUpdateStopped(key));
} else {
- let errObject;
+ let errObject = err;
if (err.responseJSON) {
errObject = err.responseJSON;
} else if (err.stack) {
errObject = {
- error: t('Unexpected error: ') + err.description,
+ error:
+ t('Unexpected error: ') +
+ (err.description || t('(no description, click to see stack trace)')),
stacktrace: err.stack,
};
} else if (err.responseText && err.responseText.indexOf('CSRF') >= 0) {
errObject = {
error: COMMON_ERR_MESSAGES.SESSION_TIMED_OUT,
};
- } else {
- errObject = {
- error: t('Unexpected error.'),
- };
}
dispatch(chartUpdateFailed(errObject, key));
}
});
+
const annotationLayers = formData.annotation_layers || [];
+
return Promise.all([
queryPromise,
dispatch(triggerQuery(false, key)),
@@ -203,29 +206,21 @@ export function runQuery(formData, force = false, timeout = 60, key) {
};
}
-export const SQLLAB_REDIRECT_FAILED = 'SQLLAB_REDIRECT_FAILED';
-export function sqllabRedirectFailed(error, key) {
- return { type: SQLLAB_REDIRECT_FAILED, error, key };
-}
-
export function redirectSQLLab(formData) {
- return function (dispatch) {
- const { url, payload } = getExploreUrlAndPayload({ formData, endpointType: 'query' });
- $.ajax({
- type: 'POST',
- url,
- data: {
- form_data: JSON.stringify(payload),
- },
- success: (response) => {
- const redirectUrl = new URI(window.location);
- redirectUrl
- .pathname('/superset/sqllab')
- .search({ datasourceKey: formData.datasource, sql: response.query });
- window.open(redirectUrl.href(), '_blank');
- },
- error: (xhr, status, error) => dispatch(sqllabRedirectFailed(error, formData.slice_id)),
- });
+ return (dispatch) => {
+ const { url } = getExploreUrlAndPayload({ formData, endpointType: 'query' });
+ return SupersetClient.get({ url })
+ .then(({ json }) => {
+ const redirectUrl = new URL(window.location);
+ redirectUrl.pathname = '/superset/sqllab';
+ for (const key of redirectUrl.searchParams.keys()) {
+ redirectUrl.searchParams.delete(key);
+ }
+ redirectUrl.searchParams.set('datasourceKey', formData.datasource);
+ redirectUrl.searchParams.set('sql', json.query);
+ window.open(redirectUrl.href, '_blank');
+ })
+ .catch(() => dispatch(addDangerToast(t('An error occurred while loading the SQL'))));
};
}
diff --git a/superset/assets/src/common.js b/superset/assets/src/common.js
index 69c25568440..4b1add9ec0c 100644
--- a/superset/assets/src/common.js
+++ b/superset/assets/src/common.js
@@ -1,14 +1,14 @@
-/* eslint-disable global-require */
+/* eslint global-require: 0, no-console: 0 */
import $ from 'jquery';
+import { SupersetClient } from '@superset-ui/core';
+import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
+
import airbnb from './modules/colorSchemes/airbnb';
import categoricalSchemes from './modules/colorSchemes/categorical';
import lyft from './modules/colorSchemes/lyft';
import { getInstance } from './modules/ColorSchemeManager';
import { toggleCheckbox } from './modules/utils';
-// Everything imported in this file ends up in the common entry file
-// be mindful of double-imports
-
$(document).ready(function () {
$(':checkbox[data-checkbox-api-prefix]').change(function () {
const $this = $(this);
@@ -22,10 +22,9 @@ $(document).ready(function () {
ev.preventDefault();
const targetUrl = ev.currentTarget.href;
- $.ajax(targetUrl)
- .then(() => {
- location.reload();
- });
+ $.ajax(targetUrl).then(() => {
+ location.reload();
+ });
});
});
@@ -37,9 +36,18 @@ getInstance()
.setDefaultSchemeName('bnbColors');
export function appSetup() {
- // A set of hacks to allow apps to run within a FAB template
+ // A set of hacks to allow apps to run within a FAB template
// this allows for the server side generated menus to function
window.$ = $;
window.jQuery = $;
require('bootstrap');
+
+ SupersetClient.configure({
+ protocol: (window.location && window.location.protocol) || '',
+ host: (window.location && window.location.host) || '',
+ })
+ .init()
+ .catch((error) => {
+ console.warn('Error initializing SupersetClient', error);
+ });
}
diff --git a/superset/assets/src/explore/components/ExploreChartPanel.jsx b/superset/assets/src/explore/components/ExploreChartPanel.jsx
index bcda75d711d..9d30284ebe9 100644
--- a/superset/assets/src/explore/components/ExploreChartPanel.jsx
+++ b/superset/assets/src/explore/components/ExploreChartPanel.jsx
@@ -62,7 +62,7 @@ class ExploreChartPanel extends React.PureComponent {
latestQueryFormData={chart.latestQueryFormData}
lastRendered={chart.lastRendered}
queryResponse={chart.queryResponse}
- queryRequest={chart.queryRequest}
+ queryController={chart.queryController}
triggerQuery={chart.triggerQuery}
/>
);
diff --git a/superset/assets/src/explore/components/ExploreViewContainer.jsx b/superset/assets/src/explore/components/ExploreViewContainer.jsx
index 34f165dc2b6..fc95cef7500 100644
--- a/superset/assets/src/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/src/explore/components/ExploreViewContainer.jsx
@@ -54,6 +54,9 @@ class ExploreViewContainer extends React.Component {
this.addHistory = this.addHistory.bind(this);
this.handleResize = this.handleResize.bind(this);
this.handlePopstate = this.handlePopstate.bind(this);
+ this.onStop = this.onStop.bind(this);
+ this.onQuery = this.onQuery.bind(this);
+ this.toggleModal = this.toggleModal.bind(this);
}
componentDidMount() {
@@ -124,7 +127,9 @@ class ExploreViewContainer extends React.Component {
}
onStop() {
- return this.props.chart.queryRequest.abort();
+ if (this.props.chart && this.props.chart.queryController) {
+ this.props.chart.queryController.abort();
+ }
}
getWidth() {
@@ -262,7 +267,7 @@ class ExploreViewContainer extends React.Component {
>
{this.state.showModal && (
@@ -271,9 +276,9 @@ class ExploreViewContainer extends React.Component {
x).length;
}
-
handleAnnotationType(annotationType) {
this.setState({
annotationType,
@@ -199,31 +210,25 @@ export default class AnnotationLayer extends React.PureComponent {
fetchOptions(annotationType, sourceType, isLoadingOptions) {
if (isLoadingOptions === true) {
if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
- $.ajax({
- type: 'GET',
- url: '/annotationlayermodelview/api/read?',
- }).then((data) => {
- const layers = data ? data.result.map(layer => ({
- value: layer.id,
- label: layer.name,
- })) : [];
+ SupersetClient.get({ endpoint: '/annotationlayermodelview/api/read?' }).then(({ json }) => {
+ const layers = json
+ ? json.result.map(layer => ({
+ value: layer.id,
+ label: layer.name,
+ }))
+ : [];
this.setState({
isLoadingOptions: false,
valueOptions: layers,
});
});
} else if (requiresQuery(sourceType)) {
- $.ajax({
- type: 'GET',
- url: '/superset/user_slices',
- }).then(data =>
+ SupersetClient.get({ endpoint: '/superset/user_slices' }).then(({ json }) =>
this.setState({
isLoadingOptions: false,
- valueOptions: data.filter(
- x => getSupportedSourceTypes(annotationType)
- .find(v => v === x.viz_type))
- .map(x => ({ value: x.id, label: x.title, slice: x }),
- ),
+ valueOptions: json
+ .filter(x => getSupportedSourceTypes(annotationType).find(v => v === x.viz_type))
+ .map(x => ({ value: x.id, label: x.title, slice: x })),
}),
);
} else {
@@ -266,26 +271,26 @@ export default class AnnotationLayer extends React.PureComponent {
}
renderValueConfiguration() {
- const { annotationType, sourceType, value,
- valueOptions, isLoadingOptions } = this.state;
+ const { annotationType, sourceType, value, valueOptions, isLoadingOptions } = this.state;
let label = '';
let description = '';
if (requiresQuery(sourceType)) {
if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
- label = t('Annotation Layer');
- description = t('Select the Annotation Layer you would like to use.');
+ label = 'Annotation Layer';
+ description = 'Select the Annotation Layer you would like to use.';
} else {
- label = t('Chart');
+ label = label = t('Chart');
description = `Use a pre defined Superset Chart as a source for annotations and overlays.
- 'your chart must be one of these visualization types:
- '[${getSupportedSourceTypes(annotationType)
- .map(x => ((x in vizTypes && 'label' in vizTypes[x]) ? vizTypes[x].label : '')).join(', ')}]'`;
+ your chart must be one of these visualization types:
+ [${getSupportedSourceTypes(annotationType)
+ .map(x => (x in vizTypes && 'label' in vizTypes[x] ? vizTypes[x].label : ''))
+ .join(', ')}]`;
}
} else if (annotationType === AnnotationTypes.FORMULA) {
- label = t('Formula');
- description = t(`Expects a formula with depending time parameter 'x'
+ label = 'Formula';
+ description = `Expects a formula with depending time parameter 'x'
in milliseconds since epoch. mathjs is used to evaluate the formulas.
- Example: '2x+5'`);
+ Example: '2x+5'`;
}
if (requiresQuery(sourceType)) {
return (
@@ -300,10 +305,11 @@ export default class AnnotationLayer extends React.PureComponent {
isLoading={isLoadingOptions}
value={value}
onChange={this.handleValue}
- validationErrors={!value ? [t('Mandatory')] : []}
+ validationErrors={!value ? ['Mandatory'] : []}
/>
);
- } if (annotationType === AnnotationTypes.FORMULA) {
+ }
+ if (annotationType === AnnotationTypes.FORMULA) {
return (
);
}
@@ -322,37 +328,43 @@ export default class AnnotationLayer extends React.PureComponent {
}
renderSliceConfiguration() {
- const { annotationType, sourceType, value, valueOptions, overrides, titleColumn,
- timeColumn, intervalEndColumn, descriptionColumns } = this.state;
+ const {
+ annotationType,
+ sourceType,
+ value,
+ valueOptions,
+ overrides,
+ titleColumn,
+ timeColumn,
+ intervalEndColumn,
+ descriptionColumns,
+ } = this.state;
const slice = (valueOptions.find(x => x.value === value) || {}).slice;
if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE && slice) {
- const columns = (slice.data.groupby || []).concat(
- (slice.data.all_columns || [])).map(x => ({ value: x, label: x }));
- const timeColumnOptions = slice.data.include_time ?
- [{ value: '__timestamp', label: '__timestamp' }].concat(columns) : columns;
+ const columns = (slice.data.groupby || [])
+ .concat(slice.data.all_columns || [])
+ .map(x => ({ value: x, label: x }));
+ const timeColumnOptions = slice.data.include_time
+ ? [{ value: '__timestamp', label: '__timestamp' }].concat(columns)
+ : columns;
return (
{
- }}
+ onSelect={() => {}}
title="Annotation Slice Configuration"
- info={
- `This section allows you to configure how to use the slice
- to generate annotations.`
- }
+ info={`This section allows you to configure how to use the slice
+ to generate annotations.`}
>
- {
- (
- annotationType === AnnotationTypes.EVENT ||
- annotationType === AnnotationTypes.INTERVAL
- ) &&
+ {(annotationType === AnnotationTypes.EVENT ||
+ annotationType === AnnotationTypes.INTERVAL) && (
this.setState({ timeColumn: v })}
/>
- }
- {
- annotationType === AnnotationTypes.INTERVAL &&
+ )}
+ {annotationType === AnnotationTypes.INTERVAL && (
this.setState({ intervalEndColumn: v })}
/>
- }
+ )}
this.setState({ titleColumn: v })}
/>
- {
- annotationType !== AnnotationTypes.TIME_SERIES &&
+ {annotationType !== AnnotationTypes.TIME_SERIES && (
this.setState({ descriptionColumns: v })}
/>
- }
+ )}
);
}
- return ('');
+ return '';
}
renderDisplayConfiguration() {
const { color, opacity, style, width, showMarkers, hideLine, annotationType } = this.state;
const colorScheme = [...getScheme(this.props.colorScheme)];
- if (color && color !== AUTOMATIC_COLOR &&
- !colorScheme.find(x => x.toLowerCase() === color.toLowerCase())) {
+ if (
+ color &&
+ color !== AUTOMATIC_COLOR &&
+ !colorScheme.find(x => x.toLowerCase() === color.toLowerCase())
+ ) {
colorScheme.push(color);
}
return (
@@ -493,12 +502,12 @@ export default class AnnotationLayer extends React.PureComponent {
this.setState({ style: v })}
@@ -506,12 +515,12 @@ export default class AnnotationLayer extends React.PureComponent {
this.setState({ opacity: v })}
@@ -530,7 +539,7 @@ export default class AnnotationLayer extends React.PureComponent {
bsSize="xsmall"
onClick={() => this.setState({ color: AUTOMATIC_COLOR })}
>
- {t('Automatic Color')}
+ Automatic Color
@@ -541,42 +550,36 @@ export default class AnnotationLayer extends React.PureComponent {
value={width}
onChange={v => this.setState({ width: v })}
/>
- {annotationType === AnnotationTypes.TIME_SERIES &&
- this.setState({ showMarkers: v })}
- />
- }
- {annotationType === AnnotationTypes.TIME_SERIES &&
- this.setState({ hideLine: v })}
- />
- }
+ {annotationType === AnnotationTypes.TIME_SERIES && (
+ this.setState({ showMarkers: v })}
+ />
+ )}
+ {annotationType === AnnotationTypes.TIME_SERIES && (
+ this.setState({ hideLine: v })}
+ />
+ )}
);
}
render() {
- const { isNew, name, annotationType,
- sourceType, show } = this.state;
+ const { isNew, name, annotationType, sourceType, show } = this.state;
const isValid = this.isValidForm();
return (
- {
- this.props.error &&
-
- ERROR: {this.props.error}
-
- }
+ {this.props.error &&
ERROR: {this.props.error}}
({ value: x, label: getAnnotationTypeLabel(x) }))}
+ options={getSupportedAnnotationTypes(this.props.vizType).map(x => ({
+ value: x,
+ label: getAnnotationTypeLabel(x),
+ }))}
value={annotationType}
onChange={this.handleAnnotationType}
/>
- {!!getSupportedSourceTypes(annotationType).length &&
+ {!!getSupportedSourceTypes(annotationType).length && (
({ value: x, label: getAnnotationSourceTypeLabels(x) }))}
+ options={getSupportedSourceTypes(annotationType).map(x => ({
+ value: x,
+ label: getAnnotationSourceTypeLabels(x),
+ }))}
value={sourceType}
onChange={this.handleAnnotationSourceType}
/>
- }
- { this.renderValueConfiguration() }
+ )}
+ {this.renderValueConfiguration()}
- { this.renderSliceConfiguration() }
- { this.renderDisplayConfiguration() }
+ {this.renderSliceConfiguration()}
+ {this.renderDisplayConfiguration()}
-