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: [superset-client] replace misc ajax calls (#6135)
Date Fri, 19 Oct 2018 18:41:47 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 a71e6eb  [superset-client] replace misc ajax calls  (#6135)
a71e6eb is described below

commit a71e6eb0a32409f0ae9f810da20a461c52f8b09e
Author: Chris Williams <williaster@users.noreply.github.com>
AuthorDate: Fri Oct 19 11:41:42 2018 -0700

    [superset-client] replace misc ajax calls  (#6135)
    
    * [superset-client][misc] replace ajax calls in DashboardTable, TableLoader, utils, common
    
    * [superset-client][misc] replace ajax calls in AsyncSelect, HeaderActions, Deck.gl
    
    * [superset-client][misc] fix tests
    
    * [superset-client] remove unneeded functional setState calls
    
    * [superset-client] make welcome a redux app for toasts
    
    * [superset-client] make Profile a redux app for toasts
    
    * [superset-client] TableLoader don't pass toast props to dom nodes
    
    * tweak deckgl Multi syntax
---
 .../javascripts/components/AsyncSelect_spec.jsx    | 104 ++++++++++++++-------
 .../assets/spec/javascripts/profile/App_spec.jsx   |   8 +-
 .../javascripts/profile/CreatedContent_spec.jsx    |  19 ++--
 .../spec/javascripts/profile/Favorites_spec.jsx    |  19 ++--
 .../javascripts/profile/RecentActivity_spec.jsx    |   6 +-
 .../javascripts/welcome/DashboardTable_spec.jsx    |  53 +++++++----
 superset/assets/src/common.js                      |   8 +-
 superset/assets/src/components/AsyncSelect.jsx     |  36 ++++---
 superset/assets/src/components/TableLoader.jsx     |  41 ++++++--
 .../dashboard/components/HeaderActionsDropdown.jsx |  24 +++--
 superset/assets/src/modules/utils.js               |  17 ++--
 superset/assets/src/profile/App.jsx                |  21 ++++-
 .../src/profile/components/RecentActivity.jsx      |   1 +
 .../src/visualizations/deckgl/Multi/Multi.jsx      |  34 ++++---
 superset/assets/src/welcome/App.jsx                |  22 ++++-
 superset/assets/src/welcome/DashboardTable.jsx     |  34 ++++---
 16 files changed, 300 insertions(+), 147 deletions(-)

diff --git a/superset/assets/spec/javascripts/components/AsyncSelect_spec.jsx b/superset/assets/spec/javascripts/components/AsyncSelect_spec.jsx
index df62c7f..e19f1c0 100644
--- a/superset/assets/spec/javascripts/components/AsyncSelect_spec.jsx
+++ b/superset/assets/spec/javascripts/components/AsyncSelect_spec.jsx
@@ -1,14 +1,22 @@
 import React from 'react';
 import Select from 'react-select';
 import { shallow } from 'enzyme';
-import sinon from 'sinon';
+import fetchMock from 'fetch-mock';
 
 import AsyncSelect from '../../../src/components/AsyncSelect';
 
 describe('AsyncSelect', () => {
+  afterAll(fetchMock.reset);
+  afterEach(fetchMock.resetHistory);
+
+  const dataEndpoint = '/chart/api/read';
+  const dataGlob = 'glob:*/chart/api/read';
+  fetchMock.get(dataGlob, []);
+  fetchMock.resetHistory();
+
   const mockedProps = {
-    dataEndpoint: '/chart/api/read',
-    onChange: sinon.spy(),
+    dataEndpoint,
+    onChange: () => {},
     placeholder: 'Select...',
     mutator: () => [
       { value: 1, label: 'main' },
@@ -16,6 +24,7 @@ describe('AsyncSelect', () => {
     ],
     valueRenderer: opt => opt.label,
   };
+
   it('is valid element', () => {
     expect(
       React.isValidElement(<AsyncSelect {...mockedProps} />),
@@ -30,52 +39,81 @@ describe('AsyncSelect', () => {
   });
 
   it('calls onChange on select change', () => {
+    const onChangeSpy = jest.fn();
     const wrapper = shallow(
-      <AsyncSelect {...mockedProps} />,
+      <AsyncSelect {...mockedProps} onChange={onChangeSpy} />,
     );
+
     wrapper.find(Select).simulate('change', { value: 1 });
-    expect(mockedProps.onChange).toHaveProperty('callCount', 1);
+    expect(onChangeSpy.mock.calls).toHaveLength(1);
   });
 
   describe('auto select', () => {
-    let server;
-    beforeEach(() => {
-      server = sinon.fakeServer.create();
-      server.respondWith([
-        200, { 'Content-Type': 'application/json' }, JSON.stringify({}),
-      ]);
-    });
-    afterEach(() => {
-      server.restore();
+    it('should not call onChange if autoSelect=false', (done) => {
+      expect.assertions(2);
+
+      const onChangeSpy = jest.fn();
+      shallow(
+        <AsyncSelect {...mockedProps} onChange={onChangeSpy} />,
+      );
+
+      setTimeout(() => {
+        expect(fetchMock.calls(dataGlob)).toHaveLength(1);
+        expect(onChangeSpy.mock.calls).toHaveLength(0);
+        done();
+      });
     });
-    it('should be off by default', () => {
+
+    it('should auto select the first option if autoSelect=true', (done) => {
+      expect.assertions(3);
+
+      const onChangeSpy = jest.fn();
       const wrapper = shallow(
-        <AsyncSelect {...mockedProps} />,
+        <AsyncSelect {...mockedProps} onChange={onChangeSpy} autoSelect />,
       );
-      wrapper.instance().fetchOptions();
-      const spy = sinon.spy(wrapper.instance(), 'onChange');
-      expect(spy.callCount).toBe(0);
+
+      setTimeout(() => {
+        expect(fetchMock.calls(dataGlob)).toHaveLength(1);
+        expect(onChangeSpy.mock.calls).toHaveLength(1);
+        expect(onChangeSpy).toBeCalledWith(wrapper.instance().state.options[0]);
+        done();
+      });
     });
-    it('should auto select first option', () => {
+
+    it('should not auto select when value prop is set and autoSelect=true', (done) =>
{
+      expect.assertions(3);
+
+      const onChangeSpy = jest.fn();
       const wrapper = shallow(
-        <AsyncSelect {...mockedProps} autoSelect />,
+        <AsyncSelect {...mockedProps} value={2} onChange={onChangeSpy} autoSelect />,
       );
-      const spy = sinon.spy(wrapper.instance(), 'onChange');
-      server.respond();
 
-      expect(spy.callCount).toBe(1);
-      expect(spy.calledWith(wrapper.instance().state.options[0])).toBe(true);
+      setTimeout(() => {
+        expect(fetchMock.calls(dataGlob)).toHaveLength(1);
+        expect(onChangeSpy.mock.calls).toHaveLength(0);
+        expect(wrapper.find(Select)).toHaveLength(1);
+        done();
+      });
     });
-    it('should not auto select when value prop is set', () => {
-      const wrapper = shallow(
-        <AsyncSelect {...mockedProps} value={2} autoSelect />,
+
+    it('should call onAsyncError if there is an error fetching options', (done) => {
+      expect.assertions(3);
+
+      const errorEndpoint = 'async/error/';
+      const errorGlob = 'glob:*async/error/';
+      fetchMock.get(errorGlob, { throws: 'error' });
+
+      const onAsyncError = jest.fn();
+      shallow(
+        <AsyncSelect {...mockedProps} dataEndpoint={errorEndpoint} onAsyncError={onAsyncError}
/>,
       );
-      const spy = sinon.spy(wrapper.instance(), 'onChange');
-      wrapper.instance().fetchOptions();
-      server.respond();
 
-      expect(spy.callCount).toBe(0);
-      expect(wrapper.find(Select)).toHaveLength(1);
+      setTimeout(() => {
+        expect(fetchMock.calls(errorGlob)).toHaveLength(1);
+        expect(onAsyncError.mock.calls).toHaveLength(1);
+        expect(onAsyncError).toBeCalledWith('error');
+        done();
+      });
     });
   });
 });
diff --git a/superset/assets/spec/javascripts/profile/App_spec.jsx b/superset/assets/spec/javascripts/profile/App_spec.jsx
index 0794512..089af0d 100644
--- a/superset/assets/spec/javascripts/profile/App_spec.jsx
+++ b/superset/assets/spec/javascripts/profile/App_spec.jsx
@@ -1,6 +1,6 @@
 import React from 'react';
 import { Col, Row, Tab } from 'react-bootstrap';
-import { mount } from 'enzyme';
+import { shallow } from 'enzyme';
 
 import { user } from './fixtures';
 import App from '../../../src/profile/components/App';
@@ -14,13 +14,15 @@ describe('App', () => {
       React.isValidElement(<App {...mockedProps} />),
     ).toBe(true);
   });
+
   it('renders 2 Col', () => {
-    const wrapper = mount(<App {...mockedProps} />);
+    const wrapper = shallow(<App {...mockedProps} />);
     expect(wrapper.find(Row)).toHaveLength(1);
     expect(wrapper.find(Col)).toHaveLength(2);
   });
+
   it('renders 4 Tabs', () => {
-    const wrapper = mount(<App {...mockedProps} />);
+    const wrapper = shallow(<App {...mockedProps} />);
     expect(wrapper.find(Tab)).toHaveLength(4);
   });
 });
diff --git a/superset/assets/spec/javascripts/profile/CreatedContent_spec.jsx b/superset/assets/spec/javascripts/profile/CreatedContent_spec.jsx
index a341c3e..4d76099 100644
--- a/superset/assets/spec/javascripts/profile/CreatedContent_spec.jsx
+++ b/superset/assets/spec/javascripts/profile/CreatedContent_spec.jsx
@@ -1,25 +1,28 @@
 import React from 'react';
-import { mount } from 'enzyme';
+import { shallow } from 'enzyme';
+import thunk from 'redux-thunk';
+import configureStore from 'redux-mock-store';
+
 import { user } from './fixtures';
 import CreatedContent from '../../../src/profile/components/CreatedContent';
 import TableLoader from '../../../src/components/TableLoader';
 
+// store needed for withToasts(TableLoader)
+const mockStore = configureStore([thunk]);
+const store = mockStore({});
 
 describe('CreatedContent', () => {
   const mockedProps = {
     user,
   };
-  it('is valid', () => {
-    expect(
-      React.isValidElement(<CreatedContent {...mockedProps} />),
-    ).toBe(true);
-  });
+
   it('renders 2 TableLoader', () => {
-    const wrapper = mount(<CreatedContent {...mockedProps} />);
+    const wrapper = shallow(<CreatedContent {...mockedProps} />, { context: { store
} });
     expect(wrapper.find(TableLoader)).toHaveLength(2);
   });
+
   it('renders 2 titles', () => {
-    const wrapper = mount(<CreatedContent {...mockedProps} />);
+    const wrapper = shallow(<CreatedContent {...mockedProps} />, { context: { store
} });
     expect(wrapper.find('h3')).toHaveLength(2);
   });
 });
diff --git a/superset/assets/spec/javascripts/profile/Favorites_spec.jsx b/superset/assets/spec/javascripts/profile/Favorites_spec.jsx
index 16efa90..1022208 100644
--- a/superset/assets/spec/javascripts/profile/Favorites_spec.jsx
+++ b/superset/assets/spec/javascripts/profile/Favorites_spec.jsx
@@ -1,25 +1,28 @@
 import React from 'react';
-import { mount } from 'enzyme';
+import { shallow } from 'enzyme';
+import thunk from 'redux-thunk';
+import configureStore from 'redux-mock-store';
 
 import { user } from './fixtures';
 import Favorites from '../../../src/profile/components/Favorites';
 import TableLoader from '../../../src/components/TableLoader';
 
+// store needed for withToasts(TableLoader)
+const mockStore = configureStore([thunk]);
+const store = mockStore({});
+
 describe('Favorites', () => {
   const mockedProps = {
     user,
   };
-  it('is valid', () => {
-    expect(
-      React.isValidElement(<Favorites {...mockedProps} />),
-    ).toBe(true);
-  });
+
   it('renders 2 TableLoader', () => {
-    const wrapper = mount(<Favorites {...mockedProps} />);
+    const wrapper = shallow(<Favorites {...mockedProps} />, { context: { store } });
     expect(wrapper.find(TableLoader)).toHaveLength(2);
   });
+
   it('renders 2 titles', () => {
-    const wrapper = mount(<Favorites {...mockedProps} />);
+    const wrapper = shallow(<Favorites {...mockedProps} />, { context: { store } });
     expect(wrapper.find('h3')).toHaveLength(2);
   });
 });
diff --git a/superset/assets/spec/javascripts/profile/RecentActivity_spec.jsx b/superset/assets/spec/javascripts/profile/RecentActivity_spec.jsx
index b6deeeb..2097451 100644
--- a/superset/assets/spec/javascripts/profile/RecentActivity_spec.jsx
+++ b/superset/assets/spec/javascripts/profile/RecentActivity_spec.jsx
@@ -1,11 +1,10 @@
 import React from 'react';
-import { mount } from 'enzyme';
+import { shallow } from 'enzyme';
 
 import { user } from './fixtures';
 import RecentActivity from '../../../src/profile/components/RecentActivity';
 import TableLoader from '../../../src/components/TableLoader';
 
-
 describe('RecentActivity', () => {
   const mockedProps = {
     user,
@@ -15,8 +14,9 @@ describe('RecentActivity', () => {
       React.isValidElement(<RecentActivity {...mockedProps} />),
     ).toBe(true);
   });
+
   it('renders a TableLoader', () => {
-    const wrapper = mount(<RecentActivity {...mockedProps} />);
+    const wrapper = shallow(<RecentActivity {...mockedProps} />);
     expect(wrapper.find(TableLoader)).toHaveLength(1);
   });
 });
diff --git a/superset/assets/spec/javascripts/welcome/DashboardTable_spec.jsx b/superset/assets/spec/javascripts/welcome/DashboardTable_spec.jsx
index a2ebf36..7192c3e 100644
--- a/superset/assets/spec/javascripts/welcome/DashboardTable_spec.jsx
+++ b/superset/assets/spec/javascripts/welcome/DashboardTable_spec.jsx
@@ -1,30 +1,47 @@
 import React from 'react';
 import { mount } from 'enzyme';
-import sinon from 'sinon';
+import thunk from 'redux-thunk';
+import configureStore from 'redux-mock-store';
+import fetchMock from 'fetch-mock';
+import { Table } from 'reactable';
 
 import DashboardTable from '../../../src/welcome/DashboardTable';
+import Loading from '../../../src/components/Loading';
 
-const $ = window.$ = require('jquery');
+// store needed for withToasts(TableLoader)
+const mockStore = configureStore([thunk]);
+const store = mockStore({});
 
+const dashboardsEndpoint = 'glob:*/dashboardasync/api/read*';
+const mockDashboards = [
+  { id: 1, url: 'url', dashboard_title: 'title' },
+];
+
+fetchMock.get(dashboardsEndpoint, { result: mockDashboards });
+
+function setup() {
+  // use mount because data fetching is triggered on mount
+  return mount(<DashboardTable />, { context: { store } });
+}
 
 describe('DashboardTable', () => {
-  const mockedProps = {};
-  let stub;
-  beforeEach(() => {
-    stub = sinon.stub($, 'getJSON');
-  });
-  afterEach(() => {
-    stub.restore();
-  });
+  afterEach(fetchMock.resetHistory);
 
-  it('is valid', () => {
-    expect(
-      React.isValidElement(<DashboardTable {...mockedProps} />),
-    ).toBe(true);
+  it('renders a Loading initially', () => {
+    const wrapper = setup();
+    expect(wrapper.find(Loading)).toHaveLength(1);
   });
-  it('renders', () => {
-    const wrapper = mount(<DashboardTable {...mockedProps} />);
-    expect(stub.callCount).toBe(1);
-    expect(wrapper.find('img')).toHaveLength(1);
+
+  it('fetches dashboards and renders a Table', (done) => {
+    const wrapper = setup();
+
+    setTimeout(() => {
+      expect(fetchMock.calls(dashboardsEndpoint)).toHaveLength(1);
+      // there's a delay between response and updating state, so manually set it
+      // rather than adding a timeout which could introduce flakiness
+      wrapper.setState({ dashaboards: mockDashboards });
+      expect(wrapper.find(Table)).toHaveLength(1);
+      done();
+    });
   });
 });
diff --git a/superset/assets/src/common.js b/superset/assets/src/common.js
index fafc84e..f479d22 100644
--- a/superset/assets/src/common.js
+++ b/superset/assets/src/common.js
@@ -21,10 +21,10 @@ $(document).ready(function () {
   $('#language-picker a').click(function (ev) {
     ev.preventDefault();
 
-    const targetUrl = ev.currentTarget.href;
-    $.ajax(targetUrl).then(() => {
-      location.reload();
-    });
+    SupersetClient.get({ endpoint: ev.currentTarget.href })
+      .then(() => {
+        location.reload();
+      });
   });
 });
 
diff --git a/superset/assets/src/components/AsyncSelect.jsx b/superset/assets/src/components/AsyncSelect.jsx
index e81a123..4c2ae81 100644
--- a/superset/assets/src/components/AsyncSelect.jsx
+++ b/superset/assets/src/components/AsyncSelect.jsx
@@ -1,10 +1,9 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import Select from 'react-select';
+import { SupersetClient } from '@superset-ui/core';
 import { t } from '../locales';
 
-const $ = window.$ = require('jquery');
-
 const propTypes = {
   dataEndpoint: PropTypes.string.isRequired,
   onChange: PropTypes.func.isRequired,
@@ -32,31 +31,38 @@ class AsyncSelect extends React.PureComponent {
       isLoading: false,
       options: [],
     };
+
+    this.onChange = this.onChange.bind(this);
   }
+
   componentDidMount() {
     this.fetchOptions();
   }
-  onChange(opt) {
-    this.props.onChange(opt);
+
+  onChange(option) {
+    this.props.onChange(option);
   }
+
   fetchOptions() {
     this.setState({ isLoading: true });
-    const mutator = this.props.mutator;
-    $.get(this.props.dataEndpoint)
-      .done((data) => {
-        this.setState({ options: mutator ? mutator(data) : data, isLoading: false });
+    const { mutator, dataEndpoint } = this.props;
 
-        if (!this.props.value && this.props.autoSelect && this.state.options.length)
{
-          this.onChange(this.state.options[0]);
+    return SupersetClient.get({ endpoint: dataEndpoint })
+      .then(({ json }) => {
+        const options = mutator ? mutator(json) : json;
+
+        this.setState({ options, isLoading: false });
+
+        if (!this.props.value && this.props.autoSelect && options.length
> 0) {
+          this.onChange(options[0]);
         }
       })
-      .fail((xhr) => {
-        this.props.onAsyncError(xhr.responseText);
-      })
-      .always(() => {
+      .catch((error) => {
+        this.props.onAsyncError(error.error || error.statusText || error);
         this.setState({ isLoading: false });
       });
   }
+
   render() {
     return (
       <div>
@@ -65,7 +71,7 @@ class AsyncSelect extends React.PureComponent {
           options={this.state.options}
           value={this.props.value}
           isLoading={this.state.isLoading}
-          onChange={this.onChange.bind(this)}
+          onChange={this.onChange}
           valueRenderer={this.props.valueRenderer}
           {...this.props}
         />
diff --git a/superset/assets/src/components/TableLoader.jsx b/superset/assets/src/components/TableLoader.jsx
index 3f51ee9..2f57ab8 100644
--- a/superset/assets/src/components/TableLoader.jsx
+++ b/superset/assets/src/components/TableLoader.jsx
@@ -1,7 +1,10 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { Table, Tr, Td } from 'reactable';
-import $ from 'jquery';
+import { SupersetClient } from '@superset-ui/core';
+
+import withToasts from '../messageToasts/enhancers/withToasts';
+import { t } from '../locales';
 import Loading from '../components/Loading';
 import '../../stylesheets/reactable-pagination.css';
 
@@ -9,9 +12,10 @@ const propTypes = {
   dataEndpoint: PropTypes.string.isRequired,
   mutator: PropTypes.func,
   columns: PropTypes.arrayOf(PropTypes.string),
+  addDangerToast: PropTypes.func.isRequired,
 };
 
-export default class TableLoader extends React.PureComponent {
+class TableLoader extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
@@ -19,20 +23,34 @@ export default class TableLoader extends React.PureComponent {
       data: [],
     };
   }
+
   componentWillMount() {
-    $.get(this.props.dataEndpoint, (data) => {
-      let actualData = data;
-      if (this.props.mutator) {
-        actualData = this.props.mutator(data);
-      }
-      this.setState({ data: actualData, isLoading: false });
-    });
+    const { dataEndpoint, mutator } = this.props;
+
+    SupersetClient.get({ endpoint: dataEndpoint })
+      .then(({ json }) => {
+        const data = mutator ? mutator(json) : json;
+        this.setState({ data, isLoading: false });
+      })
+      .catch(() => {
+        this.setState({ isLoading: false });
+        this.props.addDangerToast(t('An error occurred'));
+      });
   }
+
   render() {
     if (this.state.isLoading) {
       return <Loading />;
     }
-    const tableProps = Object.assign({}, this.props);
+
+    const {
+      addDangerToast,
+      addInfoToast,
+      addSuccessToast,
+      addWarningToast,
+      ...tableProps
+    } = this.props;
+
     let { columns } = this.props;
     if (!columns && this.state.data.length > 0) {
       columns = Object.keys(this.state.data[0]).filter(col => col[0] !== '_');
@@ -70,4 +88,7 @@ export default class TableLoader extends React.PureComponent {
     );
   }
 }
+
 TableLoader.propTypes = propTypes;
+
+export default withToasts(TableLoader);
diff --git a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
index c04e2b4..d012e34 100644
--- a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
+++ b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import $ from 'jquery';
+import { SupersetClient } from '@superset-ui/core';
 import { DropdownButton, MenuItem } from 'react-bootstrap';
 
 import CssEditor from './CssEditor';
@@ -52,14 +52,20 @@ class HeaderActionsDropdown extends React.PureComponent {
   componentWillMount() {
     injectCustomCss(this.state.css);
 
-    $.get('/csstemplateasyncmodelview/api/read', data => {
-      const cssTemplates = data.result.map(row => ({
-        value: row.template_name,
-        css: row.css,
-        label: row.template_name,
-      }));
-      this.setState({ cssTemplates });
-    });
+    SupersetClient.get({ endpoint: '/csstemplateasyncmodelview/api/read' })
+      .then(({ json }) => {
+        const cssTemplates = json.result.map(row => ({
+          value: row.template_name,
+          css: row.css,
+          label: row.template_name,
+        }));
+        this.setState({ cssTemplates });
+      })
+      .catch(() => {
+        this.props.addDangerToast(
+          t('An error occurred while fetching available CSS templates'),
+        );
+      });
   }
 
   changeCss(css) {
diff --git a/superset/assets/src/modules/utils.js b/superset/assets/src/modules/utils.js
index 491fcf5..879f7e8 100644
--- a/superset/assets/src/modules/utils.js
+++ b/superset/assets/src/modules/utils.js
@@ -1,6 +1,7 @@
 /* eslint camelcase: 0 */
 import d3 from 'd3';
 import $ from 'jquery';
+import { SupersetClient } from '@superset-ui/core';
 import { formatDate, UTC } from './dates';
 
 const siFormatter = d3.format('.3s');
@@ -119,13 +120,15 @@ function showApiMessage(resp) {
 }
 
 export function toggleCheckbox(apiUrlPrefix, selector) {
-  const apiUrl = apiUrlPrefix + $(selector)[0].checked;
-  $.get(apiUrl).fail(function (xhr) {
-    const resp = xhr.responseJSON;
-    if (resp && resp.message) {
-      showApiMessage(resp);
-    }
-  });
+  SupersetClient.get({ endpoint: apiUrlPrefix + $(selector)[0].checked })
+    .then(() => {})
+    .catch((response) => {
+      // @TODO utility function to read this
+      const resp = response.responseJSON;
+      if (resp && resp.message) {
+        showApiMessage(resp);
+      }
+    });
 }
 
 /**
diff --git a/superset/assets/src/profile/App.jsx b/superset/assets/src/profile/App.jsx
index 13146ec..3ad5bad 100644
--- a/superset/assets/src/profile/App.jsx
+++ b/superset/assets/src/profile/App.jsx
@@ -1,6 +1,12 @@
 import React from 'react';
 import { hot } from 'react-hot-loader';
+import thunk from 'redux-thunk';
+import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
+import { Provider } from 'react-redux';
+
 import App from './components/App';
+import messageToastReducer from '../messageToasts/reducers';
+import { initEnhancer } from '../reduxUtils';
 import { appSetup } from '../common';
 
 import './main.css';
@@ -10,8 +16,21 @@ appSetup();
 const profileViewContainer = document.getElementById('app');
 const bootstrap = JSON.parse(profileViewContainer.getAttribute('data-bootstrap'));
 
+const store = createStore(
+  combineReducers({
+    messageToasts: messageToastReducer,
+  }),
+  {},
+  compose(
+    applyMiddleware(thunk),
+    initEnhancer(false),
+  ),
+);
+
 const Application = () => (
-  <App user={bootstrap.user} />
+  <Provider store={store}>
+    <App user={bootstrap.user} />
+  </Provider>
 );
 
 export default hot(module)(Application);
diff --git a/superset/assets/src/profile/components/RecentActivity.jsx b/superset/assets/src/profile/components/RecentActivity.jsx
index 0a36fda..3698680 100644
--- a/superset/assets/src/profile/components/RecentActivity.jsx
+++ b/superset/assets/src/profile/components/RecentActivity.jsx
@@ -33,4 +33,5 @@ export default class RecentActivity extends React.PureComponent {
     );
   }
 }
+
 RecentActivity.propTypes = propTypes;
diff --git a/superset/assets/src/visualizations/deckgl/Multi/Multi.jsx b/superset/assets/src/visualizations/deckgl/Multi/Multi.jsx
index 64c1ec6..9bfc84f 100644
--- a/superset/assets/src/visualizations/deckgl/Multi/Multi.jsx
+++ b/superset/assets/src/visualizations/deckgl/Multi/Multi.jsx
@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import $ from 'jquery';
+import { SupersetClient } from '@superset-ui/core';
+
 import DeckGLContainer from '../DeckGLContainer';
 import { getExploreLongUrl } from '../../../explore/exploreUtils';
 import layerGenerators from '../layers';
@@ -47,19 +48,22 @@ class DeckMulti extends React.PureComponent {
         },
       };
 
-      const url = getExploreLongUrl(subsliceCopy.form_data, 'json');
-      $.get(url, (data) => {
-        const layer = layerGenerators[subsliceCopy.form_data.viz_type](
-          subsliceCopy.form_data,
-          data,
-        );
-        this.setState({
-          subSlicesLayers: {
-            ...this.state.subSlicesLayers,
-            [subsliceCopy.slice_id]: layer,
-          },
-        });
-      });
+      SupersetClient.get({
+          endpoint: getExploreLongUrl(subsliceCopy.form_data, 'json'),
+        })
+        .then(({ json }) => {
+          const layer = layerGenerators[subsliceCopy.form_data.viz_type](
+            subsliceCopy.form_data,
+            json,
+          );
+          this.setState({
+            subSlicesLayers: {
+              ...this.state.subSlicesLayers,
+              [subsliceCopy.slice_id]: layer,
+            },
+          });
+        })
+        .catch(() => {});
     });
   }
 
@@ -67,7 +71,7 @@ class DeckMulti extends React.PureComponent {
     const { payload, viewport, formData, setControlValue } = this.props;
     const { subSlicesLayers } = this.state;
 
-    const layers = Object.keys(subSlicesLayers).map(k => subSlicesLayers[k]);
+    const layers = Object.values(subSlicesLayers);
 
     return (
       <DeckGLContainer
diff --git a/superset/assets/src/welcome/App.jsx b/superset/assets/src/welcome/App.jsx
index 3528345..b22c162 100644
--- a/superset/assets/src/welcome/App.jsx
+++ b/superset/assets/src/welcome/App.jsx
@@ -1,5 +1,12 @@
 import React from 'react';
 import { hot } from 'react-hot-loader';
+import thunk from 'redux-thunk';
+import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
+import { Provider } from 'react-redux';
+
+import messageToastReducer from '../messageToasts/reducers';
+import { initEnhancer } from '../reduxUtils';
+
 import { appSetup } from '../common';
 import Welcome from './Welcome';
 
@@ -9,8 +16,21 @@ const container = document.getElementById('app');
 const bootstrap = JSON.parse(container.getAttribute('data-bootstrap'));
 const user = { ...bootstrap.user };
 
+const store = createStore(
+  combineReducers({
+    messageToasts: messageToastReducer,
+  }),
+  {},
+  compose(
+    applyMiddleware(thunk),
+    initEnhancer(false),
+  ),
+);
+
 const App = () => (
-  <Welcome user={user} />
+  <Provider store={store}>
+    <Welcome user={user} />
+  </Provider>
 );
 
 export default hot(module)(App);
diff --git a/superset/assets/src/welcome/DashboardTable.jsx b/superset/assets/src/welcome/DashboardTable.jsx
index 0f4c744..29c2988 100644
--- a/superset/assets/src/welcome/DashboardTable.jsx
+++ b/superset/assets/src/welcome/DashboardTable.jsx
@@ -1,33 +1,40 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { Table, Tr, Td, unsafe } from 'reactable';
+import { SupersetClient } from '@superset-ui/core';
+import withToasts from '../messageToasts/enhancers/withToasts';
+import { t } from '../locales';
+
 import Loading from '../components/Loading';
 import '../../stylesheets/reactable-pagination.css';
 
-const $ = window.$ = require('jquery');
-
 const propTypes = {
   search: PropTypes.string,
+  addDangerToast: PropTypes.func.isRequired,
 };
 
-export default class DashboardTable extends React.PureComponent {
+class DashboardTable extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
-      dashboards: false,
+      dashboards: [],
     };
   }
+
   componentDidMount() {
-    const url = (
-      '/dashboardasync/api/read' +
-      '?_oc_DashboardModelViewAsync=changed_on' +
-      '&_od_DashboardModelViewAsync=desc');
-    $.getJSON(url, (data) => {
-      this.setState({ dashboards: data.result });
-    });
+    SupersetClient.get({
+      endpoint: '/dashboardasync/api/read?_oc_DashboardModelViewAsync=changed_on&_od_DashboardModelViewAsync=desc',
+    })
+      .then(({ json }) => {
+         this.setState({ dashboards: json.result });
+      })
+      .catch(() => {
+        this.props.addDangerToast(t('An error occurred while fethching Dashboards'));
+      });
   }
+
   render() {
-    if (this.state.dashboards) {
+    if (this.state.dashboards.length > 0) {
       return (
         <Table
           className="table"
@@ -58,8 +65,11 @@ export default class DashboardTable extends React.PureComponent {
         </Table>
       );
     }
+
     return <Loading />;
   }
 }
 
 DashboardTable.propTypes = propTypes;
+
+export default withToasts(DashboardTable);


Mime
View raw message