diff --git a/superset/assets/cypress/integration/explore/link.test.js b/superset/assets/cypress/integration/explore/link.test.js index dc84e624d76bd..3487410a8587a 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 0f520395cba18..40bbd0d30ca05 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 e63ea984aee19..2884157a4cf13 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 ecac2e2ef6b8e..50635f12f0568 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 4d7ca2d943cf7..9ffb22745d48d 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 06b18ae6209ef..71647c83cbd0b 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(, { context: { store }, - }).dive()); + }).dive(); it('renders', () => { expect(React.isValidElement()).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 08fa24af8f89c..b233e19d42f89 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(); }); - afterEach(() => { - ajaxStub.restore(); - }); it('is valid', () => { - expect( - React.isValidElement(), - ).toBe(true); + expect(React.isValidElement()).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 ff5aaf6870a52..1260621a22d11 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 f8d8911cb7e06..77a48067b10be 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 a808949c2f3ad..8c9ef2d49e681 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 e48fe1be8feb7..704226898f1a0 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 ( -
- -
- {t('Share query')} + {t('share query')} - )} - 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 207ac4c631462..329f73165780c 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 ( - { - 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 ')} CREATE TABLE AS {t('feature to store a summarized data set that you can then explore.')} - ); + + ); } renderInvalidColumnMessage() { const invalidColumns = this.getInvalidColumns(); @@ -150,15 +148,20 @@ class ExploreResultsButton extends React.PureComponent { return (
{t('Column name(s) ')} - {invalidColumns.join(', ')} + + {invalidColumns.join(', ')} + {t('cannot be used as a column name. Please use aliases (as in ')} - SELECT count(*) + + SELECT count(*) AS my_alias ){' '} - {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.')} -
); + {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. + `)} + + ); } render() { return ( @@ -173,12 +176,9 @@ class ExploreResultsButton extends React.PureComponent { this.dialog = el; }} /> - {t('Explore')} - ); + {t('Explore')} + + ); } } 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 0839c25cb7379..0b0936458362b 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 237ed11c7b534..a3d9ddf3a93c4 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 (
diff --git a/superset/assets/src/SqlLab/components/QueryTable.jsx b/superset/assets/src/SqlLab/components/QueryTable.jsx index 76c39909199f3..f92d65e8afab7 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 = ( -
- - {time[0]}
{time[1]} -
-
- ); - q.user = ( - - ); - q.db = ( - - ); - q.started = moment(q.startDttm).format('HH:mm:ss'); - q.querylink = ( -
+ 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 = ( +
+ + {time[0]}
{time[1]} +
+
+ ); + q.user = ( -
- ); - q.sql = ( - - - - ); - if (q.resultsKey) { - q.output = ( - - {t('view results')} - - )} - modalTitle={t('Data preview')} - beforeOpen={this.openAsyncResults.bind(this, query)} - onExit={this.clearQueryResults.bind(this, query)} - modalBody={ - - } - /> ); - } 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 = ( - - ); - let errorTooltip; - if (q.errorMessage) { - errorTooltip = ( - - - + q.db = ( + ); - } - q.state = ( -
- - {errorTooltip} -
- ); - q.actions = ( -
- - - + +
+ ); + q.sql = ( + + + + ); + if (q.resultsKey) { + q.output = ( + + {t('view results')} + + } + modalTitle={t('Data preview')} + beforeOpen={this.openAsyncResults.bind(this, query)} + onExit={this.clearQueryResults.bind(this, query)} + modalBody={ + + } + /> + ); + } 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 = ( + -
- ); - return q; - }).reverse(); + ); + let errorTooltip; + if (q.errorMessage) { + errorTooltip = ( + + + + ); + } + q.state = ( +
+ + {errorTooltip} +
+ ); + q.actions = ( +
+ + + +
+ ); + return q; + }) + .reverse(); return (
); } } diff --git a/superset/assets/src/SqlLab/components/SqlEditorLeftBar.jsx b/superset/assets/src/SqlLab/components/SqlEditorLeftBar.jsx index e6e7f5dd26f86..f17f8103ec57e 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 {   ({this.state.tableOptions.length} -  {t('in')}  - - {this.props.queryEditor.schema} - ) +   + {t('in')} +   + {this.props.queryEditor.schema})
- {this.props.queryEditor.schema && + {this.props.queryEditor.schema ? ( - } + )}
{this.props.tables.map(table => ( - + ))}
- {shouldShowReset && + {shouldShowReset && ( - } + )} ); } } + SqlEditorLeftBar.propTypes = propTypes; SqlEditorLeftBar.defaultProps = defaultProps; diff --git a/superset/assets/src/SqlLab/getInitialState.js b/superset/assets/src/SqlLab/getInitialState.js index b914220580ebd..9c9210fb48668 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 7916b72d27dbd..e357111ca824f 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 593e0b0575c9d..9514b18f1e51b 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 ( - {this.props.shouldShowText && - - {this.props.text} -      - - } + {this.props.shouldShowText && this.props.text && ( + {this.props.text} + )} ({ + 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 9f7a36bce4e1f..907b239d6adf2 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 18c7b5084beba..7fa7043b38f96 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 = {}) {