superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ccwilli...@apache.org
Subject [incubator-superset] branch master updated: [SIP-4] replace SQL Lab ajax calls with `SupersetClient` (#5896)
Date Thu, 18 Oct 2018 17:40:40 GMT
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 <williaster@users.noreply.github.com>
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 {
             &nbsp;
             <small>
               ({this.state.tableOptions.length}
-              &nbsp;{t('in')}&nbsp;
-              <i>
-                {this.props.queryEditor.schema}
-              </i>)
+              &nbsp;
+              {t('in')}
+              &nbsp;
+              <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}
-            &nbsp;&nbsp;&nbsp;&nbsp;
-          </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 = {}) {


Mime
View raw message