This is an automated email from the ASF dual-hosted git repository.
ccwilliams pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new e163dfe [SIP-4] replace SQL Lab ajax calls with `SupersetClient`
(#5896)
e163dfe is described below
commit e163dfe7442b4a810c9b31d33c75f023e68c4610
Author: Chris Williams <[email protected]>
AuthorDate: Thu Oct 18 10:40:30 2018 -0700
[SIP-4] replace SQL Lab ajax calls with `SupersetClient` (#5896)
* [superset-client] replace sqllab ajax calls with SupersetClient
* [superset-client][sqllab] replace more misc ajax calls
* [superset-client][tests] call setupSupersetClient() in test shim
* [superset-client] replace more sqllab ajax calls and fix tests
* [superset-client][tests] remove commented lines
* [sqllab][superset-client] fix eslint and tests, add better error handling
tests.
* [superset-client] fix tests from rebase
* [cypress][sqllab][superset-client] fix
* [superset-client] use Promises not callbacks in getShortUrl calls
* [superset-client][short-url] don't stringify POST
* [superset-client][short-url][cypress] add data-test attribute for more
reliable test
* [cypress] remove .only() call
---
.../cypress/integration/explore/link.test.js | 19 +-
.../assets/cypress/integration/sqllab/query.js | 3 +-
superset/assets/spec/helpers/shim.js | 4 +
.../spec/javascripts/explore/chartActions_spec.js | 2 -
.../explore/components/SaveModal_spec.jsx | 2 -
.../sqllab/ExploreResultsButton_spec.jsx | 89 ++---
.../javascripts/sqllab/SqlEditorLeftBar_spec.jsx | 162 +++++----
.../assets/spec/javascripts/sqllab/actions_spec.js | 176 ++++++----
.../assets/spec/javascripts/sqllab/fixtures.js | 196 +++++------
superset/assets/src/SqlLab/actions.js | 364 +++++++++------------
.../src/SqlLab/components/CopyQueryTabUrl.jsx | 34 +-
.../src/SqlLab/components/ExploreResultsButton.jsx | 55 ++--
.../src/SqlLab/components/QueryAutoRefresh.jsx | 31 +-
.../assets/src/SqlLab/components/QuerySearch.jsx | 34 +-
.../assets/src/SqlLab/components/QueryTable.jsx | 225 ++++++-------
.../assets/src/SqlLab/components/ShareQuery.jsx | 4 +-
.../src/SqlLab/components/SqlEditorLeftBar.jsx | 111 ++++---
superset/assets/src/SqlLab/getInitialState.js | 2 +-
superset/assets/src/SqlLab/reducers.js | 47 +--
superset/assets/src/components/CopyToClipboard.jsx | 9 +-
.../assets/src/components/URLShortLinkButton.jsx | 10 +-
.../assets/src/components/URLShortLinkModal.jsx | 8 +-
superset/assets/src/utils/common.js | 50 +--
23 files changed, 880 insertions(+), 757 deletions(-)
diff --git a/superset/assets/cypress/integration/explore/link.test.js
b/superset/assets/cypress/integration/explore/link.test.js
index dc84e62..3487410 100644
--- a/superset/assets/cypress/integration/explore/link.test.js
+++ b/superset/assets/cypress/integration/explore/link.test.js
@@ -26,19 +26,22 @@ describe('Test explore links', () => {
});
it('Visit short link', () => {
+ cy.route('POST', 'r/shortner/').as('getShortUrl');
+
cy.visitChartByName('Growth Rate');
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.get('[data-test=short-link-button]').click();
- cy.get('#shorturl-popover').within(() => {
- cy.get('i[title="Copy to clipboard"]')
- .siblings()
- .first()
- .invoke('text')
- .then((text) => {
- cy.visit(text);
+
+ // explicitly wait for the url response
+ cy.wait('@getShortUrl');
+
+ cy.wait(100);
+
+ cy.get('#shorturl-popover [data-test="short-url"]').invoke('text')
+ .then((text) => {
+ cy.visit(text);
});
- });
cy.verifySliceSuccess({ waitAlias: '@getJson' });
});
diff --git a/superset/assets/cypress/integration/sqllab/query.js
b/superset/assets/cypress/integration/sqllab/query.js
index 0f52039..40bbd0d 100644
--- a/superset/assets/cypress/integration/sqllab/query.js
+++ b/superset/assets/cypress/integration/sqllab/query.js
@@ -40,6 +40,7 @@ export default () => {
it('successfully saves a query', () => {
cy.route('savedqueryviewapi/**').as('getSavedQuery');
+ cy.route('superset/tables/**').as('getTables');
const query = 'SELECT ds, gender, name, num FROM main.birth_names ORDER
BY name LIMIT 3';
const savedQueryTitle = `CYPRESS TEST QUERY ${shortid.generate()}`;
@@ -83,7 +84,7 @@ export default () => {
cy.get('table tr:first-child a[href*="savedQueryId"').click();
// will timeout without explicitly waiting here
- cy.wait('@getSavedQuery');
+ cy.wait(['@getSavedQuery', '@getTables']);
// run the saved query
cy.get('#js-sql-toolbar button')
diff --git a/superset/assets/spec/helpers/shim.js
b/superset/assets/spec/helpers/shim.js
index e63ea98..2884157 100644
--- a/superset/assets/spec/helpers/shim.js
+++ b/superset/assets/spec/helpers/shim.js
@@ -5,6 +5,8 @@ import jsdom from 'jsdom';
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
+import setupSupersetClient from './setupSupersetClient';
+
configure({ adapter: new Adapter() });
const exposedProperties = ['window', 'navigator', 'document'];
@@ -45,3 +47,5 @@ global.window.XMLHttpRequest = global.XMLHttpRequest;
global.window.location = { href: 'about:blank' };
global.window.performance = { now: () => new Date().getTime() };
global.$ = require('jquery')(global.window);
+
+setupSupersetClient();
diff --git a/superset/assets/spec/javascripts/explore/chartActions_spec.js
b/superset/assets/spec/javascripts/explore/chartActions_spec.js
index ecac2e2..50635f1 100644
--- a/superset/assets/spec/javascripts/explore/chartActions_spec.js
+++ b/superset/assets/spec/javascripts/explore/chartActions_spec.js
@@ -2,7 +2,6 @@ import fetchMock from 'fetch-mock';
import sinon from 'sinon';
import { Logger } from '../../../src/logger';
-import setupSupersetClient from '../../helpers/setupSupersetClient';
import * as exploreUtils from '../../../src/explore/exploreUtils';
import * as actions from '../../../src/chart/chartAction';
@@ -17,7 +16,6 @@ describe('chart actions', () => {
};
beforeAll(() => {
- setupSupersetClient();
setupDefaultFetchMock();
});
diff --git
a/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
b/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
index 4d7ca2d..9ffb227 100644
--- a/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
+++ b/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
@@ -10,7 +10,6 @@ import fetchMock from 'fetch-mock';
import * as exploreUtils from '../../../../src/explore/exploreUtils';
import * as saveModalActions from
'../../../../src/explore/actions/saveModalActions';
import SaveModal from '../../../../src/explore/components/SaveModal';
-import setupSupersetClient from '../../../helpers/setupSupersetClient';
describe('SaveModal', () => {
const middlewares = [thunk];
@@ -182,7 +181,6 @@ describe('SaveModal', () => {
const saveEndpoint = `glob:*/dashboardasync/api/read?_flt_0_owners=${1}`;
beforeAll(() => {
- setupSupersetClient();
fetchMock.get(saveEndpoint, mockDashboardData);
});
diff --git
a/superset/assets/spec/javascripts/sqllab/ExploreResultsButton_spec.jsx
b/superset/assets/spec/javascripts/sqllab/ExploreResultsButton_spec.jsx
index 06b18ae..71647c8 100644
--- a/superset/assets/spec/javascripts/sqllab/ExploreResultsButton_spec.jsx
+++ b/superset/assets/spec/javascripts/sqllab/ExploreResultsButton_spec.jsx
@@ -4,8 +4,8 @@ import thunk from 'redux-thunk';
import { shallow } from 'enzyme';
import sinon from 'sinon';
+import fetchMock from 'fetch-mock';
-import $ from 'jquery';
import shortid from 'shortid';
import { queries, queryWithBadColumns } from './fixtures';
import { sqlLabReducer } from '../../../src/SqlLab/reducers';
@@ -58,10 +58,10 @@ describe('ExploreResultsButton', () => {
requiresTime: true,
value: 'bar',
};
- const getExploreResultsButtonWrapper = (props = mockedProps) => (
+ const getExploreResultsButtonWrapper = (props = mockedProps) =>
shallow(<ExploreResultsButton {...props} />, {
context: { store },
- }).dive());
+ }).dive();
it('renders', () => {
expect(React.isValidElement(<ExploreResultsButton />)).toBe(true);
@@ -151,64 +151,71 @@ describe('ExploreResultsButton', () => {
datasourceName: 'mockDatasourceName',
});
- let ajaxSpy;
- let datasourceSpy;
+ const visualizeURL = '/superset/sqllab_viz/';
+ const visualizeEndpoint = `glob:*${visualizeURL}`;
+ const visualizationPayload = { table_id: 107 };
+ fetchMock.post(visualizeEndpoint, visualizationPayload);
+
beforeEach(() => {
- ajaxSpy = sinon.spy($, 'ajax');
- sinon.stub(JSON, 'parse').callsFake(() => ({ table_id: 107 }));
- sinon.stub(exploreUtils, 'getExploreUrlAndPayload').callsFake(() => ({
url: 'mockURL', payload: { datasource: '107__table' } }));
+ sinon
+ .stub(exploreUtils, 'getExploreUrlAndPayload')
+ .callsFake(() => ({ url: 'mockURL', payload: { datasource:
'107__table' } }));
sinon.spy(exploreUtils, 'exportChart');
- sinon.stub(wrapper.instance(), 'buildVizOptions').callsFake(() =>
(mockOptions));
- datasourceSpy = sinon.stub(actions, 'createDatasource');
+ sinon.stub(wrapper.instance(), 'buildVizOptions').callsFake(() =>
mockOptions);
});
afterEach(() => {
- ajaxSpy.restore();
- JSON.parse.restore();
exploreUtils.getExploreUrlAndPayload.restore();
exploreUtils.exportChart.restore();
wrapper.instance().buildVizOptions.restore();
- datasourceSpy.restore();
+ fetchMock.reset();
});
- it('should build request', () => {
+ it('should build request with correct args', (done) => {
wrapper.instance().visualize();
- expect(ajaxSpy.callCount).toBe(1);
- const spyCall = ajaxSpy.getCall(0);
- expect(spyCall.args[0].type).toBe('POST');
- expect(spyCall.args[0].url).toBe('/superset/sqllab_viz/');
- expect(spyCall.args[0].data.data).toBe(JSON.stringify(mockOptions));
+ setTimeout(() => {
+ const calls = fetchMock.calls(visualizeEndpoint);
+ expect(calls).toHaveLength(1);
+ const formData = calls[0][1].body;
+
+ Object.keys(mockOptions).forEach((key) => {
+ // eslint-disable-next-line no-unused-expressions
+ expect(formData.get(key)).toBeDefined();
+ });
+
+ done();
+ });
});
- it('should open new window', () => {
+
+ it('should export chart and add an info toast', (done) => {
const infoToastSpy = sinon.spy();
+ const datasourceSpy = sinon.stub();
- datasourceSpy.callsFake(() => {
- const d = $.Deferred();
- d.resolve('done');
- return d.promise();
- });
+ datasourceSpy.callsFake(() => Promise.resolve(visualizationPayload));
wrapper.setProps({
actions: {
- createDatasource: datasourceSpy,
addInfoToast: infoToastSpy,
+ createDatasource: datasourceSpy,
},
});
wrapper.instance().visualize();
- expect(exploreUtils.exportChart.callCount).toBe(1);
-
expect(exploreUtils.exportChart.getCall(0).args[0].datasource).toBe('107__table');
- expect(infoToastSpy.callCount).toBe(1);
- });
- it('should add error toast', () => {
- const dangerToastSpy = sinon.spy();
- datasourceSpy.callsFake(() => {
- const d = $.Deferred();
- d.reject('error message');
- return d.promise();
+ setTimeout(() => {
+ expect(datasourceSpy.callCount).toBe(1);
+ expect(exploreUtils.exportChart.callCount).toBe(1);
+
expect(exploreUtils.exportChart.getCall(0).args[0].datasource).toBe('107__table');
+ expect(infoToastSpy.callCount).toBe(1);
+ done();
});
+ });
+ it('should add error toast', (done) => {
+ const dangerToastSpy = sinon.stub(actions, 'addDangerToast');
+ const datasourceSpy = sinon.stub();
+
+ datasourceSpy.callsFake(() => Promise.reject({ error: 'error' }));
wrapper.setProps({
actions: {
@@ -218,8 +225,14 @@ describe('ExploreResultsButton', () => {
});
wrapper.instance().visualize();
- expect(exploreUtils.exportChart.callCount).toBe(0);
- expect(dangerToastSpy.callCount).toBe(1);
+
+ setTimeout(() => {
+ expect(datasourceSpy.callCount).toBe(1);
+ expect(exploreUtils.exportChart.callCount).toBe(0);
+ expect(dangerToastSpy.callCount).toBe(1);
+ dangerToastSpy.restore();
+ done();
+ });
});
});
});
diff --git a/superset/assets/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
b/superset/assets/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
index 08fa24a..b233e19 100644
--- a/superset/assets/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
+++ b/superset/assets/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
@@ -1,8 +1,8 @@
import React from 'react';
import { shallow } from 'enzyme';
import sinon from 'sinon';
+import fetchMock from 'fetch-mock';
-import $ from 'jquery';
import { table, defaultQueryEditor, databases, tables } from './fixtures';
import SqlEditorLeftBar from '../../../src/SqlLab/components/SqlEditorLeftBar';
import TableElement from '../../../src/SqlLab/components/TableElement';
@@ -23,23 +23,19 @@ describe('SqlEditorLeftBar', () => {
};
let wrapper;
- let ajaxStub;
+
beforeEach(() => {
- ajaxStub = sinon.stub($, 'get');
wrapper = shallow(<SqlEditorLeftBar {...mockedProps} />);
});
- afterEach(() => {
- ajaxStub.restore();
- });
it('is valid', () => {
- expect(
- React.isValidElement(<SqlEditorLeftBar {...mockedProps} />),
- ).toBe(true);
+ expect(React.isValidElement(<SqlEditorLeftBar {...mockedProps}
/>)).toBe(true);
});
+
it('renders a TableElement', () => {
expect(wrapper.find(TableElement)).toHaveLength(1);
});
+
describe('onDatabaseChange', () => {
it('should fetch schemas', () => {
sinon.stub(wrapper.instance(), 'fetchSchemas');
@@ -52,34 +48,42 @@ describe('SqlEditorLeftBar', () => {
expect(wrapper.state().tableOptions).toEqual([]);
});
});
+
describe('getTableNamesBySubStr', () => {
- it('should handle empty', () => (
- wrapper.instance().getTableNamesBySubStr('')
+ const GET_TABLE_NAMES_GLOB = 'glob:*/superset/tables/1/main/*';
+
+ afterEach(fetchMock.resetHistory);
+ afterAll(fetchMock.reset);
+
+ it('should handle empty', () =>
+ wrapper
+ .instance()
+ .getTableNamesBySubStr('')
.then((data) => {
expect(data).toEqual({ options: [] });
- })
- ));
+ }));
+
it('should handle table name', () => {
- const queryEditor = Object.assign({}, defaultQueryEditor,
- {
- dbId: 1,
- schema: 'main',
- });
+ const queryEditor = {
+ ...defaultQueryEditor,
+ dbId: 1,
+ schema: 'main',
+ };
+
const mockTableOptions = { options: [table] };
wrapper.setProps({ queryEditor });
- ajaxStub.callsFake(() => {
- const d = $.Deferred();
- d.resolve(mockTableOptions);
- return d.promise();
- });
+ fetchMock.get(GET_TABLE_NAMES_GLOB, mockTableOptions, { overwriteRoutes:
true });
- return wrapper.instance().getTableNamesBySubStr('my table')
+ return wrapper
+ .instance()
+ .getTableNamesBySubStr('my table')
.then((data) => {
- expect(ajaxStub.getCall(0).args[0]).toBe('/superset/tables/1/main/my
table');
+ expect(fetchMock.calls(GET_TABLE_NAMES_GLOB)).toHaveLength(1);
expect(data).toEqual(mockTableOptions);
});
});
});
+
it('dbMutator should build databases options', () => {
const options = wrapper.instance().dbMutator(databases);
expect(options).toEqual([
@@ -87,65 +91,109 @@ describe('SqlEditorLeftBar', () => {
{ value: 208, label: 'Presto - Gold' },
]);
});
+
describe('fetchTables', () => {
+ const FETCH_TABLES_GLOB =
'glob:*/superset/tables/1/main/birth_names/true/';
+ afterEach(fetchMock.resetHistory);
+ afterAll(fetchMock.reset);
+
it('should clear table options', () => {
wrapper.instance().fetchTables(1);
expect(wrapper.state().tableOptions).toEqual([]);
expect(wrapper.state().filterOptions).toBeNull();
});
+
it('should fetch table options', () => {
- ajaxStub.callsFake(() => {
- const d = $.Deferred();
- d.resolve(tables);
- return d.promise();
- });
- wrapper.instance().fetchTables(1, 'main', 'true', 'birth_names');
+ expect.assertions(2);
+ fetchMock.get(FETCH_TABLES_GLOB, tables, { overwriteRoutes: true });
-
expect(ajaxStub.getCall(0).args[0]).toBe('/superset/tables/1/main/birth_names/true/');
- expect(wrapper.state().tableLength).toBe(3);
+ return wrapper
+ .instance()
+ .fetchTables(1, 'main', true, 'birth_names')
+ .then(() => {
+ expect(fetchMock.calls(FETCH_TABLES_GLOB)).toHaveLength(1);
+ expect(wrapper.state().tableLength).toBe(3);
+ });
});
- it('should handle error', () => {
- ajaxStub.callsFake(() => {
- const d = $.Deferred();
- d.reject('error message');
- return d.promise();
+
+ it('should dispatch a danger toast on error', () => {
+ const dangerToastSpy = sinon.spy();
+
+ wrapper.setProps({
+ actions: {
+ addDangerToast: dangerToastSpy,
+ },
});
- wrapper.instance().fetchTables(1, 'main', 'birth_names');
- expect(wrapper.state().tableOptions).toEqual([]);
- expect(wrapper.state().tableLength).toBe(0);
+
+ expect.assertions(4);
+ fetchMock.get(FETCH_TABLES_GLOB, { throws: 'error' }, { overwriteRoutes:
true });
+
+ return wrapper
+ .instance()
+ .fetchTables(1, 'main', true, 'birth_names')
+ .then(() => {
+ expect(fetchMock.calls(FETCH_TABLES_GLOB)).toHaveLength(1);
+ expect(wrapper.state().tableOptions).toEqual([]);
+ expect(wrapper.state().tableLength).toBe(0);
+ expect(dangerToastSpy.callCount).toBe(1);
+ });
});
});
+
describe('fetchSchemas', () => {
+ const FETCH_SCHEMAS_GLOB = 'glob:*/superset/schemas/*';
+ afterEach(fetchMock.resetHistory);
+ afterAll(fetchMock.reset);
+
it('should fetch schema options', () => {
+ expect.assertions(2);
const schemaOptions = {
schemas: ['main', 'erf', 'superset'],
};
- ajaxStub.callsFake(() => {
- const d = $.Deferred();
- d.resolve(schemaOptions);
- return d.promise();
- });
- wrapper.instance().fetchSchemas(1);
- expect(ajaxStub.getCall(0).args[0]).toBe('/superset/schemas/1/false/');
- expect(wrapper.state().schemaOptions).toHaveLength(3);
+ fetchMock.get(FETCH_SCHEMAS_GLOB, schemaOptions, { overwriteRoutes: true
});
+
+ return wrapper
+ .instance()
+ .fetchSchemas(1)
+ .then(() => {
+ expect(fetchMock.calls(FETCH_SCHEMAS_GLOB)).toHaveLength(1);
+ expect(wrapper.state().schemaOptions).toHaveLength(3);
+ });
});
- it('should handle error', () => {
- ajaxStub.callsFake(() => {
- const d = $.Deferred();
- d.reject('error message');
- return d.promise();
+
+ it('should dispatch a danger toast on error', () => {
+ const dangerToastSpy = sinon.spy();
+
+ wrapper.setProps({
+ actions: {
+ addDangerToast: dangerToastSpy,
+ },
});
- wrapper.instance().fetchSchemas(123);
- expect(wrapper.state().schemaOptions).toEqual([]);
+
+ expect.assertions(3);
+
+ fetchMock.get(FETCH_SCHEMAS_GLOB, { throws: 'error' }, {
overwriteRoutes: true });
+
+ return wrapper
+ .instance()
+ .fetchSchemas(123)
+ .then(() => {
+ expect(fetchMock.calls(FETCH_SCHEMAS_GLOB)).toHaveLength(1);
+ expect(wrapper.state().schemaOptions).toEqual([]);
+ expect(dangerToastSpy.callCount).toBe(1);
+ });
});
});
+
describe('changeTable', () => {
beforeEach(() => {
sinon.stub(wrapper.instance(), 'fetchTables');
});
+
afterEach(() => {
wrapper.instance().fetchTables.restore();
});
+
it('test 1', () => {
wrapper.instance().changeTable({
value: 'birth_names',
@@ -153,6 +201,7 @@ describe('SqlEditorLeftBar', () => {
});
expect(wrapper.state().tableName).toBe('birth_names');
});
+
it('test 2', () => {
wrapper.instance().changeTable({
value: 'main.my_table',
@@ -161,6 +210,7 @@ describe('SqlEditorLeftBar', () => {
expect(wrapper.instance().fetchTables.getCall(0).args[1]).toBe('main');
});
});
+
it('changeSchema', () => {
sinon.stub(wrapper.instance(), 'fetchTables');
diff --git a/superset/assets/spec/javascripts/sqllab/actions_spec.js
b/superset/assets/spec/javascripts/sqllab/actions_spec.js
index ff5aaf6..1260621 100644
--- a/superset/assets/spec/javascripts/sqllab/actions_spec.js
+++ b/superset/assets/spec/javascripts/sqllab/actions_spec.js
@@ -1,123 +1,175 @@
-/* eslint-disable no-unused-expressions */
+/* eslint no-unused-expressions: 0 */
import sinon from 'sinon';
-import $ from 'jquery';
+import fetchMock from 'fetch-mock';
+
import * as actions from '../../../src/SqlLab/actions';
import { query } from './fixtures';
describe('async actions', () => {
- let ajaxStub;
let dispatch;
beforeEach(() => {
dispatch = sinon.spy();
- ajaxStub = sinon.stub($, 'ajax');
- });
- afterEach(() => {
- ajaxStub.restore();
});
+ afterEach(fetchMock.resetHistory);
+
describe('saveQuery', () => {
- it('makes the ajax request', () => {
+ const saveQueryEndpoint = 'glob:*/savedqueryviewapi/api/create';
+ fetchMock.post(saveQueryEndpoint, 'ok');
+
+ it('posts to the correct url', () => {
+ expect.assertions(1);
const thunk = actions.saveQuery(query);
- thunk((/* mockDispatch */) => {});
- expect(ajaxStub.calledOnce).toBe(true);
+
+ return thunk((/* mockDispatch */) => ({})).then(() => {
+ expect(fetchMock.calls(saveQueryEndpoint)).toHaveLength(1);
+ });
});
- it('calls correct url', () => {
- const url = '/savedqueryviewapi/api/create';
+ it('posts the correct query object', () => {
const thunk = actions.saveQuery(query);
- thunk((/* mockDispatch */) => {});
- expect(ajaxStub.getCall(0).args[0].url).toBe(url);
+
+ return thunk((/* mockDispatch */) => ({})).then(() => {
+ const call = fetchMock.calls(saveQueryEndpoint)[0];
+ const formData = call[1].body;
+ Object.keys(query).forEach((key) => {
+ expect(formData.get(key)).toBeDefined();
+ });
+ });
});
});
describe('fetchQueryResults', () => {
+ const fetchQueryEndpoint = 'glob:*/superset/results/*';
+ fetchMock.get(fetchQueryEndpoint, '{ "data": "" }');
+
const makeRequest = () => {
- const request = actions.fetchQueryResults(query);
- request(dispatch);
+ const actionThunk = actions.fetchQueryResults(query);
+ return actionThunk(dispatch);
};
- it('makes the ajax request', () => {
- makeRequest();
- expect(ajaxStub.calledOnce).toBe(true);
- });
+ it('makes the fetch request', () => {
+ expect.assertions(1);
- it('calls correct url', () => {
- const url = `/superset/results/${query.resultsKey}/`;
- makeRequest();
- expect(ajaxStub.getCall(0).args[0].url).toBe(url);
+ return makeRequest().then(() => {
+ expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1);
+ });
});
it('calls requestQueryResults', () => {
- makeRequest();
- expect(dispatch.args[0][0].type).toBe(actions.REQUEST_QUERY_RESULTS);
- });
+ expect.assertions(1);
- it('calls querySuccess on ajax success', () => {
- ajaxStub.yieldsTo('success', '{ "data": "" }');
- makeRequest();
- expect(dispatch.callCount).toBe(2);
- expect(dispatch.getCall(1).args[0].type).toBe(actions.QUERY_SUCCESS);
+ return makeRequest().then(() => {
+ expect(dispatch.args[0][0].type).toBe(actions.REQUEST_QUERY_RESULTS);
+ });
});
- it('calls queryFailed on ajax error', () => {
- ajaxStub.yieldsTo('error', { responseJSON: { error: 'error text' } });
- makeRequest();
- expect(dispatch.callCount).toBe(2);
- expect(dispatch.getCall(1).args[0].type).toBe(actions.QUERY_FAILED);
+ it('calls querySuccess on fetch success', () =>
+ makeRequest().then(() => {
+ expect(dispatch.callCount).toBe(2);
+ expect(dispatch.getCall(1).args[0].type).toBe(actions.QUERY_SUCCESS);
+ }));
+
+ it('calls queryFailed on fetch error', () => {
+ expect.assertions(2);
+ fetchMock.get(
+ fetchQueryEndpoint,
+ { throws: { error: 'error text' } },
+ { overwriteRoutes: true },
+ );
+
+ return makeRequest().then(() => {
+ expect(dispatch.callCount).toBe(2);
+ expect(dispatch.getCall(1).args[0].type).toBe(actions.QUERY_FAILED);
+ });
});
});
describe('runQuery', () => {
+ const runQueryEndpoint = 'glob:*/superset/sql_json/*';
+ fetchMock.post(runQueryEndpoint, { data: '' });
+
const makeRequest = () => {
const request = actions.runQuery(query);
- request(dispatch);
+ return request(dispatch);
};
- it('makes the ajax request', () => {
- makeRequest();
- expect(ajaxStub.calledOnce).toBe(true);
+ it('makes the fetch request', () => {
+ expect.assertions(1);
+
+ return makeRequest().then(() => {
+ expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
+ });
});
it('calls startQuery', () => {
- makeRequest();
- expect(dispatch.args[0][0].type).toBe(actions.START_QUERY);
+ expect.assertions(1);
+
+ return makeRequest().then(() => {
+ expect(dispatch.args[0][0].type).toBe(actions.START_QUERY);
+ });
+ });
+
+ it('calls querySuccess on fetch success', () => {
+ expect.assertions(3);
+
+ return makeRequest().then(() => {
+ expect(dispatch.callCount).toBe(2);
+ expect(dispatch.getCall(0).args[0].type).toBe(actions.START_QUERY);
+ expect(dispatch.getCall(1).args[0].type).toBe(actions.QUERY_SUCCESS);
+ });
});
- it('calls queryFailed on ajax error', () => {
- ajaxStub.yieldsTo('error', { responseJSON: { error: 'error text' } });
- makeRequest();
- expect(dispatch.callCount).toBe(2);
- expect(dispatch.getCall(1).args[0].type).toBe(actions.QUERY_FAILED);
+ it('calls queryFailed on fetch error', () => {
+ expect.assertions(2);
+
+ fetchMock.post(
+ runQueryEndpoint,
+ { throws: { error: 'error text' } },
+ { overwriteRoutes: true },
+ );
+
+ return makeRequest().then(() => {
+ expect(dispatch.callCount).toBe(2);
+ expect(dispatch.getCall(1).args[0].type).toBe(actions.QUERY_FAILED);
+ });
});
});
describe('postStopQuery', () => {
+ const stopQueryEndpoint = 'glob:*/superset/stop_query/*';
+ fetchMock.post(stopQueryEndpoint, {});
+
const makeRequest = () => {
const request = actions.postStopQuery(query);
- request(dispatch);
+ return request(dispatch);
};
- it('makes the ajax request', () => {
- makeRequest();
- expect(ajaxStub.calledOnce).toBe(true);
+ it('makes the fetch request', () => {
+ expect.assertions(1);
+
+ return makeRequest().then(() => {
+ expect(fetchMock.calls(stopQueryEndpoint)).toHaveLength(1);
+ });
});
+
it('calls stopQuery', () => {
- makeRequest();
- expect(dispatch.args[0][0].type).toBe(actions.STOP_QUERY);
- });
+ expect.assertions(1);
- it('calls the correct url', () => {
- const url = '/superset/stop_query/';
- makeRequest();
- expect(ajaxStub.getCall(0).args[0].url).toBe(url);
+ return makeRequest().then(() => {
+ expect(dispatch.getCall(0).args[0].type).toBe(actions.STOP_QUERY);
+ });
});
it('sends the correct data', () => {
- const data = { client_id: query.id };
- makeRequest();
- expect(ajaxStub.getCall(0).args[0].data).toEqual(data);
+ expect.assertions(1);
+
+ return makeRequest().then(() => {
+ const call = fetchMock.calls(stopQueryEndpoint)[0];
+ expect(call[1].body.get('client_id')).toBe(query.id);
+ });
});
});
});
diff --git a/superset/assets/spec/javascripts/sqllab/fixtures.js
b/superset/assets/spec/javascripts/sqllab/fixtures.js
index f8d8911..77a4806 100644
--- a/superset/assets/spec/javascripts/sqllab/fixtures.js
+++ b/superset/assets/spec/javascripts/sqllab/fixtures.js
@@ -20,32 +20,24 @@ export const table = {
indexes: [
{
unique: true,
- column_names: [
- 'username',
- ],
+ column_names: ['username'],
type: 'UNIQUE',
name: 'username',
},
{
unique: true,
- column_names: [
- 'email',
- ],
+ column_names: ['email'],
type: 'UNIQUE',
name: 'email',
},
{
unique: false,
- column_names: [
- 'created_by_fk',
- ],
+ column_names: ['created_by_fk'],
name: 'created_by_fk',
},
{
unique: false,
- column_names: [
- 'changed_by_fk',
- ],
+ column_names: ['changed_by_fk'],
name: 'changed_by_fk',
},
],
@@ -70,13 +62,9 @@ export const table = {
name: 'first_name',
keys: [
{
- column_names: [
- 'first_name',
- ],
+ column_names: ['first_name'],
name: 'slices_ibfk_1',
- referred_columns: [
- 'id',
- ],
+ referred_columns: ['id'],
referred_table: 'datasources',
type: 'fk',
referred_schema: 'carapal',
@@ -84,9 +72,7 @@ export const table = {
},
{
unique: false,
- column_names: [
- 'druid_datasource_id',
- ],
+ column_names: ['druid_datasource_id'],
type: 'index',
name: 'druid_datasource_id',
},
@@ -205,21 +191,21 @@ export const queries = [
serverId: 141,
resultsKey: null,
results: {
- columns: [{
- is_date: true,
- is_dim: false,
- name: 'ds',
- type: 'STRING',
- }, {
- is_date: false,
- is_dim: true,
- name: 'gender',
- type: 'STRING',
- }],
- data: [
- { col1: 0, col2: 1 },
- { col1: 2, col2: 3 },
+ columns: [
+ {
+ is_date: true,
+ is_dim: false,
+ name: 'ds',
+ type: 'STRING',
+ },
+ {
+ is_date: false,
+ is_dim: true,
+ name: 'gender',
+ type: 'STRING',
+ },
],
+ data: [{ col1: 0, col2: 1 }, { col1: 2, col2: 3 }],
},
},
{
@@ -237,12 +223,11 @@ export const queries = [
changedOn: 1476910572000,
tempTable: null,
userId: 1,
- executedSql: (
+ executedSql:
'SELECT * \nFROM (SELECT created_on, changed_on, id, slice_name, ' +
'druid_datasource_id, table_id, datasource_type, datasource_name, ' +
'viz_type, params, created_by_fk, changed_by_fk, description, ' +
- 'cache_timeout, perm\nFROM superset.slices) AS inner_qry \n LIMIT 1000'
- ),
+ 'cache_timeout, perm\nFROM superset.slices) AS inner_qry \n LIMIT 1000',
changed_on: '2016-10-19T20:56:12',
rows: 42,
endDttm: 1476910579693,
@@ -261,72 +246,86 @@ export const queryWithBadColumns = {
...queries[0],
results: {
data: queries[0].results.data,
- columns: [{
- is_date: true,
- is_dim: false,
- name: 'COUNT(*)',
- type: 'STRING',
- }, {
- is_date: false,
- is_dim: true,
- name: 'this_col_is_ok',
- type: 'STRING',
- }, {
- is_date: false,
- is_dim: true,
- name: 'a',
- type: 'STRING',
- }, {
- is_date: false,
- is_dim: true,
- name: '1',
- type: 'STRING',
- }, {
- is_date: false,
- is_dim: true,
- name: '123',
- type: 'STRING',
- }, {
- is_date: false,
- is_dim: true,
- name: 'CASE WHEN 1=1 THEN 1 ELSE 0 END',
- type: 'STRING',
- }],
+ columns: [
+ {
+ is_date: true,
+ is_dim: false,
+ name: 'COUNT(*)',
+ type: 'STRING',
+ },
+ {
+ is_date: false,
+ is_dim: true,
+ name: 'this_col_is_ok',
+ type: 'STRING',
+ },
+ {
+ is_date: false,
+ is_dim: true,
+ name: 'a',
+ type: 'STRING',
+ },
+ {
+ is_date: false,
+ is_dim: true,
+ name: '1',
+ type: 'STRING',
+ },
+ {
+ is_date: false,
+ is_dim: true,
+ name: '123',
+ type: 'STRING',
+ },
+ {
+ is_date: false,
+ is_dim: true,
+ name: 'CASE WHEN 1=1 THEN 1 ELSE 0 END',
+ type: 'STRING',
+ },
+ ],
},
};
export const databases = {
- result: [{
- allow_ctas: true,
- allow_dml: true,
- allow_run_async: false,
- allow_run_sync: true,
- database_name: 'main',
- expose_in_sqllab: true,
- force_ctas_schema: '',
- id: 1,
- }, {
- allow_ctas: true,
- allow_dml: false,
- allow_run_async: true,
- allow_run_sync: true,
- database_name: 'Presto - Gold',
- expose_in_sqllab: true,
- force_ctas_schema: 'tmp',
- id: 208,
- }],
+ result: [
+ {
+ allow_ctas: true,
+ allow_dml: true,
+ allow_run_async: false,
+ allow_run_sync: true,
+ database_name: 'main',
+ expose_in_sqllab: true,
+ force_ctas_schema: '',
+ id: 1,
+ },
+ {
+ allow_ctas: true,
+ allow_dml: false,
+ allow_run_async: true,
+ allow_run_sync: true,
+ database_name: 'Presto - Gold',
+ expose_in_sqllab: true,
+ force_ctas_schema: 'tmp',
+ id: 208,
+ },
+ ],
};
export const tables = {
tableLength: 3,
- options: [{
- value: 'birth_names',
- label: 'birth_names',
- }, {
- value: 'energy_usage',
- label: 'energy_usage',
- }, {
- value: 'wb_health_population',
- label: 'wb_health_population',
- }],
+ options: [
+ {
+ value: 'birth_names',
+ label: 'birth_names',
+ },
+ {
+ value: 'energy_usage',
+ label: 'energy_usage',
+ },
+ {
+ value: 'wb_health_population',
+ label: 'wb_health_population',
+ },
+ ],
};
export const stoppedQuery = {
@@ -371,6 +370,7 @@ export const initialState = {
};
export const query = {
+ id: 'clientId2353',
dbId: 1,
sql: 'SELECT * FROM something',
sqlEditorId: defaultQueryEditor.id,
diff --git a/superset/assets/src/SqlLab/actions.js
b/superset/assets/src/SqlLab/actions.js
index a808949..8c9ef2d 100644
--- a/superset/assets/src/SqlLab/actions.js
+++ b/superset/assets/src/SqlLab/actions.js
@@ -1,6 +1,6 @@
-import $ from 'jquery';
import shortid from 'shortid';
import JSONbig from 'json-bigint';
+import { SupersetClient } from '@superset-ui/core';
import { now } from '../modules/dates';
import { t } from '../locales';
@@ -43,7 +43,6 @@ export const QUERY_FAILED = 'QUERY_FAILED';
export const CLEAR_QUERY_RESULTS = 'CLEAR_QUERY_RESULTS';
export const REMOVE_DATA_PREVIEW = 'REMOVE_DATA_PREVIEW';
export const CHANGE_DATA_PREVIEW_ID = 'CHANGE_DATA_PREVIEW_ID';
-export const SAVE_QUERY = 'SAVE_QUERY';
export const CREATE_DATASOURCE_STARTED = 'CREATE_DATASOURCE_STARTED';
export const CREATE_DATASOURCE_SUCCESS = 'CREATE_DATASOURCE_SUCCESS';
@@ -58,22 +57,14 @@ export function resetState() {
}
export function saveQuery(query) {
- return (dispatch) => {
- const url = '/savedqueryviewapi/api/create';
- $.ajax({
- type: 'POST',
- url,
- data: query,
- success: () => {
- dispatch(addSuccessToast(t('Your query was saved')));
- },
- error: () => {
- dispatch(addDangerToast(t('Your query could not be saved')));
- },
- dataType: 'json',
- });
- return { type: SAVE_QUERY };
- };
+ return dispatch =>
+ SupersetClient.post({
+ endpoint: '/savedqueryviewapi/api/create',
+ postPayload: query,
+ stringify: false,
+ })
+ .then(() => dispatch(addSuccessToast(t('Your query was saved'))))
+ .catch(() => dispatch(addDangerToast(t('Your query could not be
saved'))));
}
export function startQuery(query) {
@@ -81,7 +72,7 @@ export function startQuery(query) {
id: query.id ? query.id : shortid.generate(),
progress: 0,
startDttm: now(),
- state: (query.runAsync) ? 'pending' : 'running',
+ state: query.runAsync ? 'pending' : 'running',
cached: false,
});
return { type: START_QUERY, query };
@@ -111,41 +102,30 @@ export function requestQueryResults(query) {
return { type: REQUEST_QUERY_RESULTS, query };
}
-function getErrorLink(err) {
- let link = '';
- if (err.responseJSON && err.responseJSON.link) {
- link = err.responseJSON.link;
- }
- return link;
-}
-
export function fetchQueryResults(query) {
return function (dispatch) {
dispatch(requestQueryResults(query));
- const sqlJsonUrl = `/superset/results/${query.resultsKey}/`;
- $.ajax({
- type: 'GET',
- dataType: 'text',
- url: sqlJsonUrl,
- success(results) {
- const parsedResults = JSONbig.parse(results);
- dispatch(querySuccess(query, parsedResults));
- },
- error(err) {
- let msg = t('Failed at retrieving results from the results backend');
- if (err.responseJSON && err.responseJSON.error) {
- msg = err.responseJSON.error;
- }
- dispatch(queryFailed(query, msg, getErrorLink(err)));
- },
- });
+
+ return SupersetClient.get({
+ endpoint: `/superset/results/${query.resultsKey}/`,
+ parseMethod: 'text',
+ })
+ .then(({ text = '{}' }) => {
+ const bigIntJson = JSONbig.parse(text);
+ dispatch(querySuccess(query, bigIntJson));
+ })
+ .catch((error) => {
+ const message = error.error || error.statusText || t('Failed at
retrieving results');
+
+ return dispatch(queryFailed(query, message, error.link));
+ });
};
}
export function runQuery(query) {
return function (dispatch) {
dispatch(startQuery(query));
- const sqlJsonRequest = {
+ const postPayload = {
client_id: query.id,
database_id: query.dbId,
json: true,
@@ -158,59 +138,38 @@ export function runQuery(query) {
select_as_cta: query.ctas,
templateParams: query.templateParams,
};
- const sqlJsonUrl = '/superset/sql_json/' + window.location.search;
- $.ajax({
- type: 'POST',
- dataType: 'json',
- url: sqlJsonUrl,
- data: sqlJsonRequest,
- success(results) {
+
+ return SupersetClient.post({
+ endpoint: `/superset/sql_json/${window.location.search}`,
+ postPayload,
+ stringify: false,
+ })
+ .then(({ json }) => {
if (!query.runAsync) {
- dispatch(querySuccess(query, results));
- }
- },
- error(err, textStatus, errorThrown) {
- let msg;
- try {
- msg = err.responseJSON.error;
- } catch (e) {
- if (err.responseText !== undefined) {
- msg = err.responseText;
- }
+ dispatch(querySuccess(query, json));
}
- if (msg === null) {
- if (errorThrown) {
- msg = `[${textStatus}] ${errorThrown}`;
- } else {
- msg = t('Unknown error');
- }
+ })
+ .catch((error) => {
+ let message = error.error || error.statusText || t('Unknown error');
+ if (message.includes('CSRF token')) {
+ message = COMMON_ERR_MESSAGES.SESSION_TIMED_OUT;
}
- if (msg.indexOf('CSRF token') > 0) {
- msg = COMMON_ERR_MESSAGES.SESSION_TIMED_OUT;
- }
- dispatch(queryFailed(query, msg, getErrorLink(err)));
- },
- });
+ // @TODO how to verify link?
+ dispatch(queryFailed(query, message, error.link));
+ });
};
}
export function postStopQuery(query) {
return function (dispatch) {
- const stopQueryUrl = '/superset/stop_query/';
- const stopQueryRequestData = { client_id: query.id };
- dispatch(stopQuery(query));
- $.ajax({
- type: 'POST',
- dataType: 'json',
- url: stopQueryUrl,
- data: stopQueryRequestData,
- success() {
- dispatch(addSuccessToast(t('Query was stopped.')));
- },
- error() {
- dispatch(addDangerToast(t('Failed at stopping query.')));
- },
- });
+ return SupersetClient.post({
+ endpoint: '/superset/stop_query/',
+ postPayload: { client_id: query.id },
+ stringify: false,
+ })
+ .then(() => dispatch(stopQuery(query)))
+ .then(() => dispatch(addSuccessToast(t('Query was stopped.'))))
+ .catch(() => dispatch(addDangerToast(t('Failed at stopping query. ') +
`'${query.id}'`)));
};
}
@@ -280,59 +239,69 @@ export function mergeTable(table, query) {
export function addTable(query, tableName, schemaName) {
return function (dispatch) {
- let table = {
+ const table = {
dbId: query.dbId,
queryEditorId: query.id,
schema: schemaName,
name: tableName,
};
- dispatch(mergeTable(Object.assign({}, table, {
- isMetadataLoading: true,
- isExtraMetadataLoading: true,
- expanded: false,
- })));
-
- let url = `/superset/table/${query.dbId}/${tableName}/${schemaName}/`;
- $.get(url, (data) => {
- const dataPreviewQuery = {
- id: shortid.generate(),
- dbId: query.dbId,
- sql: data.selectStar,
- tableName,
- sqlEditorId: null,
- tab: '',
- runAsync: false,
- ctas: false,
- };
- // Merge table to tables in state
- const newTable = Object.assign({}, table, data, {
- expanded: true,
- isMetadataLoading: false,
- });
- dispatch(mergeTable(newTable, dataPreviewQuery));
- // Run query to get preview data for table
- dispatch(runQuery(dataPreviewQuery));
- })
- .fail(() => {
- const newTable = Object.assign({}, table, {
- isMetadataLoading: false,
- });
- dispatch(mergeTable(newTable));
- dispatch(addDangerToast(t('Error occurred while fetching table
metadata')));
- });
-
- url =
`/superset/extra_table_metadata/${query.dbId}/${tableName}/${schemaName}/`;
- $.get(url, (data) => {
- table = Object.assign({}, table, data, { isExtraMetadataLoading: false
});
- dispatch(mergeTable(table));
+ dispatch(
+ mergeTable({
+ ...table,
+ isMetadataLoading: true,
+ isExtraMetadataLoading: true,
+ expanded: false,
+ }),
+ );
+
+ SupersetClient.get({ endpoint:
`/superset/table/${query.dbId}/${tableName}/${schemaName}/` })
+ .then(({ json }) => {
+ const dataPreviewQuery = {
+ id: shortid.generate(),
+ dbId: query.dbId,
+ sql: json.selectStar,
+ tableName,
+ sqlEditorId: null,
+ tab: '',
+ runAsync: false,
+ ctas: false,
+ };
+ const newTable = {
+ ...table,
+ ...json,
+ expanded: true,
+ isMetadataLoading: false,
+ };
+
+ return Promise.all([
+ dispatch(mergeTable(newTable, dataPreviewQuery)), // Merge table to
tables in state
+ dispatch(runQuery(dataPreviewQuery)), // Run query to get preview
data for table
+ ]);
+ })
+ .catch(() =>
+ Promise.all([
+ dispatch(
+ mergeTable({
+ ...table,
+ isMetadataLoading: false,
+ }),
+ ),
+ dispatch(addDangerToast(t('Error occurred while fetching table
metadata'))),
+ ]),
+ );
+
+ SupersetClient.get({
+ endpoint:
`/superset/extra_table_metadata/${query.dbId}/${tableName}/${schemaName}/`,
})
- .fail(() => {
- const newTable = Object.assign({}, table, {
- isExtraMetadataLoading: false,
- });
- dispatch(mergeTable(newTable));
- dispatch(addDangerToast(t('Error occurred while fetching table
metadata')));
- });
+ .then(({ json }) =>
+ dispatch(mergeTable({ ...table, ...json, isExtraMetadataLoading: false
})),
+ )
+ .catch(() =>
+ Promise.all([
+ dispatch(mergeTable({ ...table, isExtraMetadataLoading: false })),
+ dispatch(addDangerToast(t('Error occurred while fetching table
metadata'))),
+ ]),
+ );
};
}
@@ -379,74 +348,61 @@ export function persistEditorHeight(queryEditor,
currentHeight) {
export function popStoredQuery(urlId) {
return function (dispatch) {
- $.ajax({
- type: 'GET',
- url: `/kv/${urlId}`,
- success: (data) => {
- const newQuery = JSON.parse(data);
- const queryEditorProps = {
- title: newQuery.title ? newQuery.title : t('shared query'),
- dbId: newQuery.dbId ? parseInt(newQuery.dbId, 10) : null,
- schema: newQuery.schema ? newQuery.schema : null,
- autorun: newQuery.autorun ? newQuery.autorun : false,
- sql: newQuery.sql ? newQuery.sql : 'SELECT ...',
- };
- dispatch(addQueryEditor(queryEditorProps));
- },
- error: () => {
- dispatch(addDangerToast(t('The query couldn\'t be loaded')));
- },
- });
+ return SupersetClient.get({ endpoint: `/kv/${urlId}` })
+ .then(({ json }) =>
+ dispatch(
+ addQueryEditor({
+ title: json.title ? json.title : t('Sjsonhared query'),
+ dbId: json.dbId ? parseInt(json.dbId, 10) : null,
+ schema: json.schema ? json.schema : null,
+ autorun: json.autorun ? json.autorun : false,
+ sql: json.sql ? json.sql : 'SELECT ...',
+ }),
+ ),
+ )
+ .catch(() => dispatch(addDangerToast(t("The query couldn't be
loaded"))));
};
}
export function popSavedQuery(saveQueryId) {
return function (dispatch) {
- $.ajax({
- type: 'GET',
- url: `/savedqueryviewapi/api/get/${saveQueryId}`,
- success: (data) => {
- const sq = data.result;
+ return SupersetClient.get({ endpoint:
`/savedqueryviewapi/api/get/${saveQueryId}` })
+ .then(({ json }) => {
+ const { result } = json;
const queryEditorProps = {
- title: sq.label,
- dbId: sq.db_id ? parseInt(sq.db_id, 10) : null,
- schema: sq.schema,
+ title: result.label,
+ dbId: result.db_id ? parseInt(result.db_id, 10) : null,
+ schema: result.schema,
autorun: false,
- sql: sq.sql,
+ sql: result.sql,
};
- dispatch(addQueryEditor(queryEditorProps));
- },
- error: () => {
- dispatch(addDangerToast(t('The query couldn\'t be loaded')));
- },
- });
+ return dispatch(addQueryEditor(queryEditorProps));
+ })
+ .catch(() => dispatch(addDangerToast(t("The query couldn't be
loaded"))));
};
}
export function popDatasourceQuery(datasourceKey, sql) {
return function (dispatch) {
- $.ajax({
- type: 'GET',
- url:
`/superset/fetch_datasource_metadata?datasourceKey=${datasourceKey}`,
- success: (metadata) => {
- const queryEditorProps = {
- title: 'Query ' + metadata.name,
- dbId: metadata.database.id,
- schema: metadata.schema,
- autorun: sql !== undefined,
- sql: sql || metadata.select_star,
- };
- dispatch(addQueryEditor(queryEditorProps));
- },
- error: () => {
- dispatch(addDangerToast(t("The datasource couldn't be loaded")));
- },
- });
+ return SupersetClient.get({
+ endpoint:
`/superset/fetch_datasource_metadata?datasourceKey=${datasourceKey}`,
+ })
+ .then(({ json }) =>
+ dispatch(
+ addQueryEditor({
+ title: 'Query ' + json.name,
+ dbId: json.database.id,
+ schema: json.schema,
+ autorun: sql !== undefined,
+ sql: sql || json.select_star,
+ }),
+ ),
+ )
+ .catch(() => dispatch(addDangerToast(t("The datasource couldn't be
loaded"))));
};
}
export function createDatasourceStarted() {
return { type: CREATE_DATASOURCE_STARTED };
}
-export function createDatasourceSuccess(response) {
- const data = JSON.parse(response);
+export function createDatasourceSuccess(data) {
const datasource = `${data.table_id}__table`;
return { type: CREATE_DATASOURCE_SUCCESS, datasource };
}
@@ -454,25 +410,23 @@ export function createDatasourceFailed(err) {
return { type: CREATE_DATASOURCE_FAILED, err };
}
-export function createDatasource(vizOptions, context) {
+export function createDatasource(vizOptions) {
return (dispatch) => {
dispatch(createDatasourceStarted());
+ return SupersetClient.post({
+ endpoint: '/superset/sqllab_viz/',
+ postPayload: { data: vizOptions },
+ })
+ .then(({ json }) => {
+ const data = JSON.parse(json);
+ dispatch(createDatasourceSuccess(data));
- return $.ajax({
- type: 'POST',
- url: '/superset/sqllab_viz/',
- async: false,
- data: {
- data: JSON.stringify(vizOptions),
- },
- context,
- dataType: 'json',
- success: (resp) => {
- dispatch(createDatasourceSuccess(resp));
- },
- error: () => {
+ return Promise.resolve(data);
+ })
+ .catch(() => {
dispatch(createDatasourceFailed(t('An error occurred while creating
the data source')));
- },
- });
+
+ return Promise.reject();
+ });
};
}
diff --git a/superset/assets/src/SqlLab/components/CopyQueryTabUrl.jsx
b/superset/assets/src/SqlLab/components/CopyQueryTabUrl.jsx
index e48fe1b..7042268 100644
--- a/superset/assets/src/SqlLab/components/CopyQueryTabUrl.jsx
+++ b/superset/assets/src/SqlLab/components/CopyQueryTabUrl.jsx
@@ -1,3 +1,4 @@
+/* eslint no-alert: 0 */
import React from 'react';
import PropTypes from 'prop-types';
import CopyToClipboard from '../../components/CopyToClipboard';
@@ -9,6 +10,11 @@ const propTypes = {
};
export default class CopyQueryTabUrl extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.getUrl = this.getUrl.bind(this);
+ }
+
getUrl(callback) {
const qe = this.props.queryEditor;
const sharedQuery = {
@@ -18,24 +24,34 @@ export default class CopyQueryTabUrl extends
React.PureComponent {
autorun: qe.autorun,
sql: qe.sql,
};
- storeQuery(sharedQuery, callback);
+
+ // the fetch call to get a url is async, but execCommand('copy') must be
sync
+ // get around this with 2 timeouts. calling a timeout from within a
timeout is not considered
+ // a short-lived, user-initiated sync event
+ let url;
+ storeQuery(sharedQuery).then((shareUrl) => { url = shareUrl; });
+ const longTimeout = setTimeout(() => { if (url) callback(url); }, 750);
+ setTimeout(() => {
+ if (url) {
+ callback(url);
+ clearTimeout(longTimeout);
+ }
+ }, 150);
+
}
render() {
return (
<CopyToClipboard
inMenu
- copyNode={(
+ copyNode={
<div>
- <div className="icon-container">
- <i className="fa fa-clipboard" />
- </div>
- <span>{t('Share query')}</span>
+ <i className="fa fa-clipboard" /> <span>{t('share query')}</span>
</div>
- )}
- tooltipText={t('Copy URL to clipboard')}
+ }
+ tooltipText={t('copy URL to clipboard')}
shouldShowText={false}
- getText={this.getUrl.bind(this)}
+ getText={this.getUrl}
/>
);
}
diff --git a/superset/assets/src/SqlLab/components/ExploreResultsButton.jsx
b/superset/assets/src/SqlLab/components/ExploreResultsButton.jsx
index 207ac4c..329f731 100644
--- a/superset/assets/src/SqlLab/components/ExploreResultsButton.jsx
+++ b/superset/assets/src/SqlLab/components/ExploreResultsButton.jsx
@@ -54,9 +54,7 @@ class ExploreResultsButton extends React.PureComponent {
this.dialog.show({
title: t('Explore'),
body: msg,
- actions: [
- Dialog.DefaultAction('Ok', () => {}, 'btn-primary'),
- ],
+ actions: [Dialog.DefaultAction('Ok', () => {}, 'btn-primary')],
bsSize: 'large',
bsStyle: 'warning',
onHide: (dialog) => {
@@ -106,10 +104,10 @@ class ExploreResultsButton extends React.PureComponent {
};
}
visualize() {
- this.props.actions.createDatasource(this.buildVizOptions(), this)
- .done((resp) => {
+ this.props.actions
+ .createDatasource(this.buildVizOptions())
+ .then((data) => {
const columns = this.getColumns();
- const data = JSON.parse(resp);
const formData = {
datasource: `${data.table_id}__table`,
metrics: [],
@@ -119,28 +117,28 @@ class ExploreResultsButton extends React.PureComponent {
all_columns: columns.map(c => c.name),
row_limit: 1000,
};
+
this.props.actions.addInfoToast(t('Creating a data source and creating
a new tab'));
// open new window for data visualization
exportChart(formData);
})
- .fail(() => {
- this.props.actions.addDangerToast(this.props.errorMessage);
+ .catch(() => {
+ this.props.actions.addDangerToast(this.props.errorMessage || t('An
error occurred'));
});
}
renderTimeoutWarning() {
return (
<Alert bsStyle="warning">
- {
- t('This query took %s seconds to run, ',
Math.round(this.getQueryDuration())) +
+ {t('This query took %s seconds to run, ',
Math.round(this.getQueryDuration())) +
t('and the explore view times out at %s seconds ',
this.props.timeout) +
t('following this flow will most likely lead to your query timing
out. ') +
t('We recommend your summarize your data further before following
that flow. ') +
- t('If activated you can use the ')
- }
+ t('If activated you can use the ')}
<strong>CREATE TABLE AS </strong>
{t('feature to store a summarized data set that you can then
explore.')}
- </Alert>);
+ </Alert>
+ );
}
renderInvalidColumnMessage() {
const invalidColumns = this.getInvalidColumns();
@@ -150,15 +148,20 @@ class ExploreResultsButton extends React.PureComponent {
return (
<div>
{t('Column name(s) ')}
- <code><strong>{invalidColumns.join(', ')} </strong></code>
+ <code>
+ <strong>{invalidColumns.join(', ')} </strong>
+ </code>
{t('cannot be used as a column name. Please use aliases (as in ')}
- <code>SELECT count(*)
+ <code>
+ SELECT count(*)
<strong>AS my_alias</strong>
</code>){' '}
- {t('limited to alphanumeric characters and underscores. Column aliases
ending with ' +
- 'double underscores followed by a numeric value are not allowed for
reasons ' +
- 'discussed in Github issue #5739.')}
- </div>);
+ {t(`limited to alphanumeric characters and underscores. Column aliases
ending with
+ double underscores followed by a numeric value are not allowed for
reasons
+ discussed in Github issue #5739.
+ `)}
+ </div>
+ );
}
render() {
return (
@@ -173,12 +176,9 @@ class ExploreResultsButton extends React.PureComponent {
this.dialog = el;
}}
/>
- <InfoTooltipWithTrigger
- icon="line-chart"
- placement="top"
- label="explore"
- /> {t('Explore')}
- </Button>);
+ <InfoTooltipWithTrigger icon="line-chart" placement="top"
label="explore" /> {t('Explore')}
+ </Button>
+ );
}
}
ExploreResultsButton.propTypes = propTypes;
@@ -198,4 +198,7 @@ function mapDispatchToProps(dispatch) {
}
export { ExploreResultsButton };
-export default connect(mapStateToProps,
mapDispatchToProps)(ExploreResultsButton);
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(ExploreResultsButton);
diff --git a/superset/assets/src/SqlLab/components/QueryAutoRefresh.jsx
b/superset/assets/src/SqlLab/components/QueryAutoRefresh.jsx
index 0839c25..0b09364 100644
--- a/superset/assets/src/SqlLab/components/QueryAutoRefresh.jsx
+++ b/superset/assets/src/SqlLab/components/QueryAutoRefresh.jsx
@@ -2,9 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
-import * as Actions from '../actions';
+import { SupersetClient } from '@superset-ui/core';
-const $ = require('jquery');
+import * as Actions from '../actions';
const QUERY_UPDATE_FREQ = 2000;
const QUERY_UPDATE_BUFFER_MS = 5000;
@@ -19,16 +19,19 @@ class QueryAutoRefresh extends React.PureComponent {
}
shouldCheckForQueries() {
// if there are started or running queries, this method should return true
- const { queries } = this.props;
+ const { queries, queriesLastUpdate } = this.props;
const now = new Date().getTime();
- return Object.values(queries)
- .some(
+
+ return (
+ queriesLastUpdate > 0 &&
+ Object.values(queries).some(
q => ['running', 'started', 'pending', 'fetching',
'rendering'].indexOf(q.state) >= 0 &&
now - q.startDttm < MAX_QUERY_AGE_TO_POLL,
- );
+ )
+ );
}
startTimer() {
- if (!(this.timer)) {
+ if (!this.timer) {
this.timer = setInterval(this.stopwatch.bind(this), QUERY_UPDATE_FREQ);
}
}
@@ -39,10 +42,11 @@ class QueryAutoRefresh extends React.PureComponent {
stopwatch() {
// only poll /superset/queries/ if there are started or running queries
if (this.shouldCheckForQueries()) {
- const url = `/superset/queries/${this.props.queriesLastUpdate -
QUERY_UPDATE_BUFFER_MS}`;
- $.getJSON(url, (data) => {
- if (Object.keys(data).length > 0) {
- this.props.actions.refreshQueries(data);
+ SupersetClient.get({
+ endpoint: `/superset/queries/${this.props.queriesLastUpdate -
QUERY_UPDATE_BUFFER_MS}`,
+ }).then(({ json }) => {
+ if (Object.keys(json).length > 0) {
+ this.props.actions.refreshQueries(json);
}
});
}
@@ -70,4 +74,7 @@ function mapDispatchToProps(dispatch) {
};
}
-export default connect(mapStateToProps, mapDispatchToProps)(QueryAutoRefresh);
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(QueryAutoRefresh);
diff --git a/superset/assets/src/SqlLab/components/QuerySearch.jsx
b/superset/assets/src/SqlLab/components/QuerySearch.jsx
index 237ed11..a3d9ddf 100644
--- a/superset/assets/src/SqlLab/components/QuerySearch.jsx
+++ b/superset/assets/src/SqlLab/components/QuerySearch.jsx
@@ -2,6 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Button } from 'react-bootstrap';
import Select from 'react-select';
+import { SupersetClient } from '@superset-ui/core';
+
import Loading from '../../components/Loading';
import QueryTable from './QueryTable';
import {
@@ -14,8 +16,6 @@ import { STATUS_OPTIONS, TIME_OPTIONS } from '../constants';
import AsyncSelect from '../../components/AsyncSelect';
import { t } from '../../locales';
-const $ = require('jquery');
-
const propTypes = {
actions: PropTypes.object.isRequired,
height: PropTypes.string.isRequired,
@@ -49,28 +49,34 @@ class QuerySearch extends React.PureComponent {
this.onUserClicked = this.onUserClicked.bind(this);
this.onDbClicked = this.onDbClicked.bind(this);
}
+
componentDidMount() {
this.refreshQueries();
}
+
onUserClicked(userId) {
this.setState({ userId }, () => {
this.refreshQueries();
});
}
+
onDbClicked(dbId) {
this.setState({ databaseId: dbId }, () => {
this.refreshQueries();
});
}
+
onChange(db) {
const val = db ? db.value : null;
this.setState({ databaseId: val });
}
+
onKeyDown(event) {
if (event.keyCode === 13) {
this.refreshQueries();
}
}
+
getTimeFromSelection(selection) {
switch (selection) {
case 'now':
@@ -91,37 +97,45 @@ class QuerySearch extends React.PureComponent {
return null;
}
}
+
changeFrom(user) {
const val = user ? user.value : null;
this.setState({ from: val });
}
+
changeTo(status) {
const val = status ? status.value : null;
this.setState({ to: val });
}
+
changeUser(user) {
const val = user ? user.value : null;
this.setState({ userId: val });
}
+
insertParams(baseUrl, params) {
const validParams = params.filter(function (p) {
return p !== '';
});
return baseUrl + '?' + validParams.join('&');
}
+
changeStatus(status) {
const val = status ? status.value : null;
this.setState({ status: val });
}
+
changeSearch(event) {
this.setState({ searchText: event.target.value });
}
+
userLabel(user) {
if (user.first_name && user.last_name) {
return user.first_name + ' ' + user.last_name;
}
return user.username;
}
+
userMutator(data) {
const options = [];
for (let i = 0; i < data.pks.length; i++) {
@@ -129,6 +143,7 @@ class QuerySearch extends React.PureComponent {
}
return options;
}
+
dbMutator(data) {
const options = data.result.map(db => ({ value: db.id, label:
db.database_name }));
this.props.actions.setDatabases(data.result);
@@ -137,6 +152,7 @@ class QuerySearch extends React.PureComponent {
}
return options;
}
+
refreshQueries() {
this.setState({ queriesLoading: true });
const params = [
@@ -148,13 +164,15 @@ class QuerySearch extends React.PureComponent {
this.state.to ? `to=${this.getTimeFromSelection(this.state.to)}` : '',
];
- const url = this.insertParams('/superset/search_queries', params);
- $.getJSON(url, (data, status) => {
- if (status === 'success') {
- this.setState({ queriesArray: data, queriesLoading: false });
- }
- });
+ SupersetClient.get({ endpoint:
this.insertParams('/superset/search_queries', params) })
+ .then(({ json }) => {
+ this.setState({ queriesArray: json, queriesLoading: false });
+ })
+ .catch(() => {
+ this.props.actions.addDangerToast(t('An error occurred when refreshing
queries'));
+ });
}
+
render() {
return (
<div>
diff --git a/superset/assets/src/SqlLab/components/QueryTable.jsx
b/superset/assets/src/SqlLab/components/QueryTable.jsx
index 76c3990..f92d65e 100644
--- a/superset/assets/src/SqlLab/components/QueryTable.jsx
+++ b/superset/assets/src/SqlLab/components/QueryTable.jsx
@@ -27,7 +27,6 @@ const defaultProps = {
onDbClicked: () => {},
};
-
class QueryTable extends React.PureComponent {
constructor(props) {
super(props);
@@ -49,7 +48,7 @@ class QueryTable extends React.PureComponent {
schema,
sql,
};
- storeQuery(newQuery, this.callback);
+ storeQuery(newQuery).then(url => this.callback(url));
}
hideVisualizeModal() {
this.setState({ showVisualizeModal: false });
@@ -74,123 +73,125 @@ class QueryTable extends React.PureComponent {
this.props.actions.removeQuery(query);
}
render() {
- const data = this.props.queries.map((query) => {
- const q = Object.assign({}, query);
- if (q.endDttm) {
- q.duration = fDuration(q.startDttm, q.endDttm);
- }
- const time = moment(q.startDttm).format().split('T');
- q.time = (
- <div>
- <span>
- {time[0]} <br /> {time[1]}
- </span>
- </div>
- );
- q.user = (
- <button
- className="btn btn-link btn-xs"
- onClick={this.props.onUserClicked.bind(this, q.userId)}
- >
- {q.user}
- </button>
- );
- q.db = (
- <button
- className="btn btn-link btn-xs"
- onClick={this.props.onDbClicked.bind(this, q.dbId)}
- >
- {q.db}
- </button>
- );
- q.started = moment(q.startDttm).format('HH:mm:ss');
- q.querylink = (
- <div style={{ width: '100px' }}>
+ const data = this.props.queries
+ .map((query) => {
+ const q = Object.assign({}, query);
+ if (q.endDttm) {
+ q.duration = fDuration(q.startDttm, q.endDttm);
+ }
+ const time = moment(q.startDttm)
+ .format()
+ .split('T');
+ q.time = (
+ <div>
+ <span>
+ {time[0]} <br /> {time[1]}
+ </span>
+ </div>
+ );
+ q.user = (
<button
className="btn btn-link btn-xs"
- onClick={this.openQuery.bind(this, q.dbId, q.schema, q.sql)}
+ onClick={this.props.onUserClicked.bind(this, q.userId)}
>
- <i className="fa fa-external-link" />{t('Open in SQL Editor')}
+ {q.user}
</button>
- </div>
- );
- q.sql = (
- <Well>
- <HighlightedSql sql={q.sql} rawSql={q.executedSql} shrink
maxWidth={60} />
- </Well>
- );
- if (q.resultsKey) {
- q.output = (
- <ModalTrigger
- bsSize="large"
- className="ResultsModal"
- triggerNode={(
- <Label
- bsStyle="info"
- style={{ cursor: 'pointer' }}
- >
- {t('view results')}
- </Label>
- )}
- modalTitle={t('Data preview')}
- beforeOpen={this.openAsyncResults.bind(this, query)}
- onExit={this.clearQueryResults.bind(this, query)}
- modalBody={
- <ResultSet showSql query={query} actions={this.props.actions}
height={400} />
- }
- />
);
- } else {
- // if query was run using ctas and force_ctas_schema was set
- // tempTable will have the schema
- const schemaUsed = q.ctas && q.tempTable && q.tempTable.includes('.')
? '' : q.schema;
- q.output = [schemaUsed, q.tempTable].filter(v => (v)).join('.');
- }
- q.progress = (
- <ProgressBar
- style={{ width: '75px' }}
- striped
- now={q.progress}
- label={`${q.progress}%`}
- />
- );
- let errorTooltip;
- if (q.errorMessage) {
- errorTooltip = (
- <Link tooltip={q.errorMessage}>
- <i className="fa fa-exclamation-circle text-danger" />
- </Link>
+ q.db = (
+ <button
+ className="btn btn-link btn-xs"
+ onClick={this.props.onDbClicked.bind(this, q.dbId)}
+ >
+ {q.db}
+ </button>
);
- }
- q.state = (
- <div>
- <QueryStateLabel query={query} />
- {errorTooltip}
- </div>
- );
- q.actions = (
- <div style={{ width: '75px' }}>
- <Link
- className="fa fa-pencil m-r-3"
- onClick={this.restoreSql.bind(this, query)}
- tooltip={t('Overwrite text in editor with a query on this table')}
- placement="top"
- />
- <Link
- className="fa fa-plus-circle m-r-3"
- onClick={this.openQueryInNewTab.bind(this, query)}
- tooltip={t('Run query in a new tab')}
- placement="top"
- />
- <Link
- className="fa fa-trash m-r-3"
- tooltip={t('Remove query from log')}
- onClick={this.removeQuery.bind(this, query)}
+ q.started = moment(q.startDttm).format('HH:mm:ss');
+ q.querylink = (
+ <div style={{ width: '100px' }}>
+ <button
+ className="btn btn-link btn-xs"
+ onClick={this.openQuery.bind(this, q.dbId, q.schema, q.sql)}
+ >
+ <i className="fa fa-external-link" />
+ {t('Open in SQL Editor')}
+ </button>
+ </div>
+ );
+ q.sql = (
+ <Well>
+ <HighlightedSql sql={q.sql} rawSql={q.executedSql} shrink
maxWidth={60} />
+ </Well>
+ );
+ if (q.resultsKey) {
+ q.output = (
+ <ModalTrigger
+ bsSize="large"
+ className="ResultsModal"
+ triggerNode={
+ <Label bsStyle="info" style={{ cursor: 'pointer' }}>
+ {t('view results')}
+ </Label>
+ }
+ modalTitle={t('Data preview')}
+ beforeOpen={this.openAsyncResults.bind(this, query)}
+ onExit={this.clearQueryResults.bind(this, query)}
+ modalBody={
+ <ResultSet showSql query={query} actions={this.props.actions}
height={400} />
+ }
+ />
+ );
+ } else {
+ // if query was run using ctas and force_ctas_schema was set
+ // tempTable will have the schema
+ const schemaUsed = q.ctas && q.tempTable &&
q.tempTable.includes('.') ? '' : q.schema;
+ q.output = [schemaUsed, q.tempTable].filter(v => v).join('.');
+ }
+ q.progress = (
+ <ProgressBar
+ style={{ width: '75px' }}
+ striped
+ now={q.progress}
+ label={`${q.progress}%`}
/>
- </div>
- );
- return q;
- }).reverse();
+ );
+ let errorTooltip;
+ if (q.errorMessage) {
+ errorTooltip = (
+ <Link tooltip={q.errorMessage}>
+ <i className="fa fa-exclamation-circle text-danger" />
+ </Link>
+ );
+ }
+ q.state = (
+ <div>
+ <QueryStateLabel query={query} />
+ {errorTooltip}
+ </div>
+ );
+ q.actions = (
+ <div style={{ width: '75px' }}>
+ <Link
+ className="fa fa-pencil m-r-3"
+ onClick={this.restoreSql.bind(this, query)}
+ tooltip={t('Overwrite text in editor with a query on this
table')}
+ placement="top"
+ />
+ <Link
+ className="fa fa-plus-circle m-r-3"
+ onClick={this.openQueryInNewTab.bind(this, query)}
+ tooltip={t('Run query in a new tab')}
+ placement="top"
+ />
+ <Link
+ className="fa fa-trash m-r-3"
+ tooltip={t('Remove query from log')}
+ onClick={this.removeQuery.bind(this, query)}
+ />
+ </div>
+ );
+ return q;
+ })
+ .reverse();
return (
<div className="QueryTable">
<Table
diff --git a/superset/assets/src/SqlLab/components/ShareQuery.jsx
b/superset/assets/src/SqlLab/components/ShareQuery.jsx
index 56556ac..48e8330 100644
--- a/superset/assets/src/SqlLab/components/ShareQuery.jsx
+++ b/superset/assets/src/SqlLab/components/ShareQuery.jsx
@@ -5,7 +5,7 @@ import CopyQueryTabUrl from './CopyQueryTabUrl';
import Button from '../../components/Button';
import { t } from '../../locales';
-export default class ShareQueryBtn extends CopyQueryTabUrl {
+export default class ShareQuery extends CopyQueryTabUrl {
render() {
return (
<CopyToClipboard
@@ -16,7 +16,7 @@ export default class ShareQueryBtn extends CopyQueryTabUrl {
)}
tooltipText={t('copy URL to clipboard')}
shouldShowText={false}
- getText={this.getUrl.bind(this)}
+ getText={this.getUrl}
/>);
}
}
diff --git a/superset/assets/src/SqlLab/components/SqlEditorLeftBar.jsx
b/superset/assets/src/SqlLab/components/SqlEditorLeftBar.jsx
index e6e7f5d..f17f810 100644
--- a/superset/assets/src/SqlLab/components/SqlEditorLeftBar.jsx
+++ b/superset/assets/src/SqlLab/components/SqlEditorLeftBar.jsx
@@ -3,14 +3,13 @@ import PropTypes from 'prop-types';
import { ControlLabel, Button } from 'react-bootstrap';
import Select from 'react-virtualized-select';
import createFilterOptions from 'react-select-fast-filter-options';
+import { SupersetClient } from '@superset-ui/core';
import TableElement from './TableElement';
import AsyncSelect from '../../components/AsyncSelect';
import RefreshLabel from '../../components/RefreshLabel';
import { t } from '../../locales';
-const $ = require('jquery');
-
const propTypes = {
queryEditor: PropTypes.object.isRequired,
height: PropTypes.number.isRequired,
@@ -34,61 +33,74 @@ class SqlEditorLeftBar extends React.PureComponent {
tableOptions: [],
};
}
+
componentWillMount() {
this.fetchSchemas(this.props.queryEditor.dbId);
this.fetchTables(this.props.queryEditor.dbId,
this.props.queryEditor.schema);
}
+
onDatabaseChange(db, force) {
const val = db ? db.value : null;
- this.setState({ schemaOptions: [], tableOptions: [] });
+ this.setState(() => ({ schemaOptions: [], tableOptions: [] }));
this.props.actions.queryEditorSetSchema(this.props.queryEditor, null);
this.props.actions.queryEditorSetDb(this.props.queryEditor, val);
if (db) {
this.fetchSchemas(val, force || false);
}
}
+
getTableNamesBySubStr(input) {
if (!this.props.queryEditor.dbId || !input) {
return Promise.resolve({ options: [] });
}
- const url = `/superset/tables/${this.props.queryEditor.dbId}/` +
- `${this.props.queryEditor.schema}/${input}`;
- return $.get(url).then(data => ({ options: data.options }));
+
+ return SupersetClient.get({
+ endpoint: `/superset/tables/${this.props.queryEditor.dbId}/${
+ this.props.queryEditor.schema
+ }/${input}`,
+ }).then(({ json }) => ({ options: json.options }));
}
+
dbMutator(data) {
const options = data.result.map(db => ({ value: db.id, label:
db.database_name }));
this.props.actions.setDatabases(data.result);
if (data.result.length === 0) {
- this.props.actions.addDangerToast(t('It seems you don\'t have access to
any database'));
+ this.props.actions.addDangerToast(t("It seems you don't have access to
any database"));
}
return options;
}
+
resetState() {
this.props.actions.resetState();
}
+
fetchTables(dbId, schema, force, substr) {
// This can be large so it shouldn't be put in the Redux store
const forceRefresh = force || false;
if (dbId && schema) {
- this.setState({ tableLoading: true, tableOptions: [] });
- const url =
`/superset/tables/${dbId}/${schema}/${substr}/${forceRefresh}/`;
- $.get(url).done((data) => {
- const filterOptions = createFilterOptions({ options: data.options });
- this.setState({
- filterOptions,
- tableLoading: false,
- tableOptions: data.options,
- tableLength: data.tableLength,
+ this.setState(() => ({ tableLoading: true, tableOptions: [] }));
+ const endpoint =
`/superset/tables/${dbId}/${schema}/${substr}/${forceRefresh}/`;
+
+ return SupersetClient.get({ endpoint })
+ .then(({ json }) => {
+ const filterOptions = createFilterOptions({ options: json.options });
+ this.setState(() => ({
+ filterOptions,
+ tableLoading: false,
+ tableOptions: json.options,
+ tableLength: json.tableLength,
+ }));
+ })
+ .catch(() => {
+ this.setState(() => ({ tableLoading: false, tableOptions: [],
tableLength: 0 }));
+ this.props.actions.addDangerToast(t('Error while fetching table
list'));
});
- })
- .fail(() => {
- this.setState({ tableLoading: false, tableOptions: [], tableLength: 0
});
- this.props.actions.addDangerToast(t('Error while fetching table
list'));
- });
- } else {
- this.setState({ tableLoading: false, tableOptions: [], filterOptions:
null });
}
+
+ this.setState(() => ({ tableLoading: false, tableOptions: [],
filterOptions: null }));
+ return Promise.resolve();
}
+
changeTable(tableOpt) {
if (!tableOpt) {
this.setState({ tableName: '' });
@@ -108,27 +120,34 @@ class SqlEditorLeftBar extends React.PureComponent {
}
this.props.actions.addTable(this.props.queryEditor, tableName, schemaName);
}
+
changeSchema(schemaOpt, force) {
- const schema = (schemaOpt) ? schemaOpt.value : null;
+ const schema = schemaOpt ? schemaOpt.value : null;
this.props.actions.queryEditorSetSchema(this.props.queryEditor, schema);
this.fetchTables(this.props.queryEditor.dbId, schema, force);
}
+
fetchSchemas(dbId, force) {
const actualDbId = dbId || this.props.queryEditor.dbId;
const forceRefresh = force || false;
if (actualDbId) {
this.setState({ schemaLoading: true });
- const url = `/superset/schemas/${actualDbId}/${forceRefresh}/`;
- $.get(url).done((data) => {
- const schemaOptions = data.schemas.map(s => ({ value: s, label: s }));
- this.setState({ schemaOptions, schemaLoading: false });
- })
- .fail(() => {
- this.setState({ schemaLoading: false, schemaOptions: [] });
- this.props.actions.addDangerToast(t('Error while fetching schema
list'));
- });
+ const endpoint = `/superset/schemas/${actualDbId}/${forceRefresh}/`;
+
+ return SupersetClient.get({ endpoint })
+ .then(({ json }) => {
+ const schemaOptions = json.schemas.map(s => ({ value: s, label: s
}));
+ this.setState({ schemaOptions, schemaLoading: false });
+ })
+ .catch(() => {
+ this.setState({ schemaLoading: false, schemaOptions: [] });
+ this.props.actions.addDangerToast(t('Error while fetching schema
list'));
+ });
}
+
+ return Promise.resolve();
}
+
closePopover(ref) {
this.refs[ref].hide();
}
@@ -206,15 +225,15 @@ class SqlEditorLeftBar extends React.PureComponent {
<small>
({this.state.tableOptions.length}
- {t('in')}
- <i>
- {this.props.queryEditor.schema}
- </i>)
+
+ {t('in')}
+
+ <i>{this.props.queryEditor.schema}</i>)
</small>
</ControlLabel>
<div className="row">
<div className="col-md-11 col-xs-11" style={{ paddingRight: '2px'
}}>
- {this.props.queryEditor.schema &&
+ {this.props.queryEditor.schema ? (
<Select
name="select-table"
ref="selectTable"
@@ -225,8 +244,7 @@ class SqlEditorLeftBar extends React.PureComponent {
filterOptions={this.state.filterOptions}
options={this.state.tableOptions}
/>
- }
- {!this.props.queryEditor.schema &&
+ ) : (
<Select
async
name="async-select-table"
@@ -237,7 +255,7 @@ class SqlEditorLeftBar extends React.PureComponent {
onChange={this.changeTable.bind(this)}
loadOptions={this.getTableNamesBySubStr.bind(this)}
/>
- }
+ )}
</div>
<div className="col-md-1 col-xs-1" style={{ paddingTop: '8px',
paddingLeft: '0px' }}>
<RefreshLabel
@@ -253,24 +271,21 @@ class SqlEditorLeftBar extends React.PureComponent {
<div className="scrollbar-container">
<div className="scrollbar-content" style={{ height:
tableMetaDataHeight }}>
{this.props.tables.map(table => (
- <TableElement
- table={table}
- key={table.id}
- actions={this.props.actions}
- />
+ <TableElement table={table} key={table.id}
actions={this.props.actions} />
))}
</div>
</div>
</div>
- {shouldShowReset &&
+ {shouldShowReset && (
<Button bsSize="small" bsStyle="danger"
onClick={this.resetState.bind(this)}>
<i className="fa fa-bomb" /> {t('Reset State')}
</Button>
- }
+ )}
</div>
);
}
}
+
SqlEditorLeftBar.propTypes = propTypes;
SqlEditorLeftBar.defaultProps = defaultProps;
diff --git a/superset/assets/src/SqlLab/getInitialState.js
b/superset/assets/src/SqlLab/getInitialState.js
index b914220..9c9210f 100644
--- a/superset/assets/src/SqlLab/getInitialState.js
+++ b/superset/assets/src/SqlLab/getInitialState.js
@@ -22,7 +22,7 @@ export default function getInitialState({ defaultDbId,
...restBootstrapData }) {
queryEditors: [defaultQueryEditor],
tabHistory: [defaultQueryEditor.id],
tables: [],
- queriesLastUpdate: 0,
+ queriesLastUpdate: Date.now(),
activeSouthPaneTab: 'Results',
...restBootstrapData,
},
diff --git a/superset/assets/src/SqlLab/reducers.js
b/superset/assets/src/SqlLab/reducers.js
index 7916b72..e357111 100644
--- a/superset/assets/src/SqlLab/reducers.js
+++ b/superset/assets/src/SqlLab/reducers.js
@@ -25,13 +25,14 @@ export const sqlLabReducer = function (state = {}, action) {
return addToArr(newState, 'queryEditors', action.queryEditor);
},
[actions.CLONE_QUERY_TO_NEW_TAB]() {
- const progenitor = state.queryEditors.find(qe =>
- qe.id === state.tabHistory[state.tabHistory.length - 1]);
+ const progenitor = state.queryEditors.find(
+ qe => qe.id === state.tabHistory[state.tabHistory.length - 1],
+ );
const qe = {
id: shortid.generate(),
title: t('Copy of %s', progenitor.title),
- dbId: (action.query.dbId) ? action.query.dbId : null,
- schema: (action.query.schema) ? action.query.schema : null,
+ dbId: action.query.dbId ? action.query.dbId : null,
+ schema: action.query.schema ? action.query.schema : null,
autorun: true,
sql: action.query.sql,
};
@@ -67,10 +68,11 @@ export const sqlLabReducer = function (state = {}, action) {
let existingTable;
state.tables.forEach((xt) => {
if (
- xt.dbId === at.dbId &&
- xt.queryEditorId === at.queryEditorId &&
- xt.schema === at.schema &&
- xt.name === at.name) {
+ xt.dbId === at.dbId &&
+ xt.queryEditorId === at.queryEditorId &&
+ xt.schema === at.schema &&
+ xt.name === at.name
+ ) {
existingTable = xt;
}
});
@@ -83,7 +85,7 @@ export const sqlLabReducer = function (state = {}, action) {
at.id = shortid.generate();
// for new table, associate Id of query for data preview
at.dataPreviewQueryId = null;
- let newState = addToArr(state, 'tables', at, true);
+ let newState = addToArr(state, 'tables', at);
if (action.query) {
newState = alterInArr(newState, 'tables', at, { dataPreviewQueryId:
action.query.id });
}
@@ -96,8 +98,7 @@ export const sqlLabReducer = function (state = {}, action) {
const queries = Object.assign({}, state.queries);
delete queries[action.table.dataPreviewQueryId];
const newState = alterInArr(state, 'tables', action.table, {
dataPreviewQueryId: null });
- return Object.assign(
- {}, newState, { queries });
+ return Object.assign({}, newState, { queries });
},
[actions.CHANGE_DATA_PREVIEW_ID]() {
const queries = Object.assign({}, state.queries);
@@ -111,8 +112,11 @@ export const sqlLabReducer = function (state = {}, action)
{
newTables.push(xt);
}
});
- return Object.assign(
- {}, state, { queries, tables: newTables, activeSouthPaneTab:
action.newQuery.id });
+ return Object.assign({}, state, {
+ queries,
+ tables: newTables,
+ activeSouthPaneTab: action.newQuery.id,
+ });
},
[actions.COLLAPSE_TABLE]() {
return alterInArr(state, 'tables', action.table, { expanded: false });
@@ -125,8 +129,10 @@ export const sqlLabReducer = function (state = {}, action)
{
if (action.query.sqlEditorId) {
const qe = getFromArr(state.queryEditors, action.query.sqlEditorId);
if (qe.latestQueryId && state.queries[qe.latestQueryId]) {
- const newResults = Object.assign(
- {}, state.queries[qe.latestQueryId].results, { data: [], query:
null });
+ const newResults = Object.assign({},
state.queries[qe.latestQueryId].results, {
+ data: [],
+ query: null,
+ });
const q = Object.assign({}, state.queries[qe.latestQueryId], {
results: newResults });
const queries = Object.assign({}, state.queries, { [q.id]: q });
newState = Object.assign({}, state, { queries });
@@ -202,7 +208,9 @@ export const sqlLabReducer = function (state = {}, action) {
return alterInArr(state, 'queryEditors', action.queryEditor, { sql:
action.sql });
},
[actions.QUERY_EDITOR_SET_TEMPLATE_PARAMS]() {
- return alterInArr(state, 'queryEditors', action.queryEditor, {
templateParams: action.templateParams });
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ templateParams: action.templateParams,
+ });
},
[actions.QUERY_EDITOR_SET_SELECTED_TEXT]() {
return alterInArr(state, 'queryEditors', action.queryEditor, {
selectedText: action.sql });
@@ -211,7 +219,9 @@ export const sqlLabReducer = function (state = {}, action) {
return alterInArr(state, 'queryEditors', action.queryEditor, { autorun:
action.autorun });
},
[actions.QUERY_EDITOR_PERSIST_HEIGHT]() {
- return alterInArr(state, 'queryEditors', action.queryEditor, { height:
action.currentHeight });
+ return alterInArr(state, 'queryEditors', action.queryEditor, {
+ height: action.currentHeight,
+ });
},
[actions.SET_DATABASES]() {
const databases = {};
@@ -227,8 +237,7 @@ export const sqlLabReducer = function (state = {}, action) {
let queriesLastUpdate = state.queriesLastUpdate;
for (const id in action.alteredQueries) {
const changedQuery = action.alteredQueries[id];
- if (!state.queries.hasOwnProperty(id) ||
- state.queries[id].state !== 'stopped') {
+ if (!state.queries.hasOwnProperty(id) || state.queries[id].state !==
'stopped') {
if (changedQuery.changedOn > queriesLastUpdate) {
queriesLastUpdate = changedQuery.changedOn;
}
diff --git a/superset/assets/src/components/CopyToClipboard.jsx
b/superset/assets/src/components/CopyToClipboard.jsx
index 593e0b0..9514b18 100644
--- a/superset/assets/src/components/CopyToClipboard.jsx
+++ b/superset/assets/src/components/CopyToClipboard.jsx
@@ -96,12 +96,9 @@ export default class CopyToClipboard extends React.Component
{
renderLink() {
return (
<span>
- {this.props.shouldShowText &&
- <span>
- {this.props.text}
-
- </span>
- }
+ {this.props.shouldShowText && this.props.text && (
+ <span className="m-r-5"
data-test="short-url">{this.props.text}</span>
+ )}
<OverlayTrigger
placement="top"
style={{ cursor: 'pointer' }}
diff --git a/superset/assets/src/components/URLShortLinkButton.jsx
b/superset/assets/src/components/URLShortLinkButton.jsx
index 86c1b59..b0570c0 100644
--- a/superset/assets/src/components/URLShortLinkButton.jsx
+++ b/superset/assets/src/components/URLShortLinkButton.jsx
@@ -23,14 +23,14 @@ class URLShortLinkButton extends React.Component {
this.getCopyUrl = this.getCopyUrl.bind(this);
}
- onShortUrlSuccess(data) {
- this.setState({
- shortUrl: data,
- });
+ onShortUrlSuccess(shortUrl) {
+ this.setState(() => ({
+ shortUrl,
+ }));
}
getCopyUrl() {
- getShortUrl(this.props.url, this.onShortUrlSuccess,
this.props.addDangerToast);
+
getShortUrl(this.props.url).then(this.onShortUrlSuccess).catch(this.props.addDangerToast);
}
renderPopover() {
diff --git a/superset/assets/src/components/URLShortLinkModal.jsx
b/superset/assets/src/components/URLShortLinkModal.jsx
index 9f7a36b..907b239 100644
--- a/superset/assets/src/components/URLShortLinkModal.jsx
+++ b/superset/assets/src/components/URLShortLinkModal.jsx
@@ -27,10 +27,8 @@ class URLShortLinkModal extends React.Component {
this.getCopyUrl = this.getCopyUrl.bind(this);
}
- onShortUrlSuccess(data) {
- this.setState({
- shortUrl: data,
- });
+ onShortUrlSuccess(shortUrl) {
+ this.setState(() => ({ shortUrl }));
}
setModalRef(ref) {
@@ -38,7 +36,7 @@ class URLShortLinkModal extends React.Component {
}
getCopyUrl() {
- getShortUrl(this.props.url, this.onShortUrlSuccess,
this.props.addDangerToast);
+
getShortUrl(this.props.url).then(this.onShortUrlSuccess).catch(this.props.addDangerToast);
}
render() {
diff --git a/superset/assets/src/utils/common.js
b/superset/assets/src/utils/common.js
index 18c7b50..7fa7043 100644
--- a/superset/assets/src/utils/common.js
+++ b/superset/assets/src/utils/common.js
@@ -1,5 +1,5 @@
/* eslint global-require: 0 */
-import $ from 'jquery';
+import { SupersetClient } from '@superset-ui/core';
import { t } from '../locales';
const d3 = require('d3');
@@ -17,7 +17,7 @@ export function kmToPixels(kilometers, latitude, zoomLevel) {
// Algorithm from: http://wiki.openstreetmap.org/wiki/Zoom_levels
const latitudeRad = latitude * (Math.PI / 180);
// Seems like the zoomLevel is off by one
- const kmPerPixel = EARTH_CIRCUMFERENCE_KM * Math.cos(latitudeRad) /
Math.pow(2, zoomLevel + 9);
+ const kmPerPixel = (EARTH_CIRCUMFERENCE_KM * Math.cos(latitudeRad)) /
Math.pow(2, zoomLevel + 9);
return d3.round(kilometers / kmPerPixel, 2);
}
@@ -27,7 +27,7 @@ export function isNumeric(num) {
export function rgbLuminance(r, g, b) {
// Formula: https://en.wikipedia.org/wiki/Relative_luminance
- return (LUMINANCE_RED_WEIGHT * r) + (LUMINANCE_GREEN_WEIGHT * g) +
(LUMINANCE_BLUE_WEIGHT * b);
+ return LUMINANCE_RED_WEIGHT * r + LUMINANCE_GREEN_WEIGHT * g +
LUMINANCE_BLUE_WEIGHT * b;
}
export function getParamFromQuery(query, param) {
@@ -41,19 +41,14 @@ export function getParamFromQuery(query, param) {
return null;
}
-export function storeQuery(query, callback) {
- $.ajax({
- type: 'POST',
- url: '/kv/store/',
- async: false,
- data: {
- data: JSON.stringify(query),
- },
- success: (data) => {
- const baseUrl = window.location.origin + window.location.pathname;
- const url = `${baseUrl}?id=${JSON.parse(data).id}`;
- callback(url);
- },
+export function storeQuery(query) {
+ return SupersetClient.post({
+ endpoint: '/kv/store/',
+ postPayload: { data: query },
+ }).then((response) => {
+ const baseUrl = window.location.origin + window.location.pathname;
+ const url = `${baseUrl}?id=${response.json.id}`;
+ return url;
});
}
@@ -69,22 +64,13 @@ export function getParamsFromUrl() {
return newParams;
}
-export function getShortUrl(longUrl, callback, onError) {
- $.ajax({
- type: 'POST',
- url: '/r/shortner/',
- async: false,
- data: {
- data: '/' + longUrl,
- },
- success: callback,
- error: () => {
- if (onError) {
- onError('Error getting the short URL');
- }
- callback(longUrl);
- },
- });
+export function getShortUrl(longUrl) {
+ return SupersetClient.post({
+ endpoint: '/r/shortner/',
+ postPayload: { data: `/${longUrl}` }, // note: url should contain 2x '/'
to redirect properly
+ parseMethod: 'text',
+ stringify: false, // the url saves with an extra set of string quotes
without this
+ }).then(({ text }) => text);
}
export function supersetURL(rootUrl, getParams = {}) {