From a71e6eb0a32409f0ae9f810da20a461c52f8b09e Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 19 Oct 2018 11:41:42 -0700 Subject: [PATCH] [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 --- .../components/AsyncSelect_spec.jsx | 104 ++++++++++++------ .../spec/javascripts/profile/App_spec.jsx | 8 +- .../profile/CreatedContent_spec.jsx | 19 ++-- .../javascripts/profile/Favorites_spec.jsx | 19 ++-- .../profile/RecentActivity_spec.jsx | 6 +- .../welcome/DashboardTable_spec.jsx | 53 ++++++--- superset/assets/src/common.js | 8 +- .../assets/src/components/AsyncSelect.jsx | 36 +++--- .../assets/src/components/TableLoader.jsx | 41 +++++-- .../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 +++- .../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 df62c7fafd87e..e19f1c0f3b1d0 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(), @@ -30,52 +39,81 @@ describe('AsyncSelect', () => { }); it('calls onChange on select change', () => { + const onChangeSpy = jest.fn(); const wrapper = shallow( - , + , ); + 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( + , + ); + + 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( - , + , ); - 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( - , + , ); - 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( - , + + 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( + , ); - 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 07945120a9334..089af0d482650 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(), ).toBe(true); }); + it('renders 2 Col', () => { - const wrapper = mount(); + const wrapper = shallow(); expect(wrapper.find(Row)).toHaveLength(1); expect(wrapper.find(Col)).toHaveLength(2); }); + it('renders 4 Tabs', () => { - const wrapper = mount(); + const wrapper = shallow(); 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 a341c3e47fa6a..4d7609935e122 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(), - ).toBe(true); - }); + it('renders 2 TableLoader', () => { - const wrapper = mount(); + const wrapper = shallow(, { context: { store } }); expect(wrapper.find(TableLoader)).toHaveLength(2); }); + it('renders 2 titles', () => { - const wrapper = mount(); + const wrapper = shallow(, { 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 16efa908e41c1..1022208b2af7b 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(), - ).toBe(true); - }); + it('renders 2 TableLoader', () => { - const wrapper = mount(); + const wrapper = shallow(, { context: { store } }); expect(wrapper.find(TableLoader)).toHaveLength(2); }); + it('renders 2 titles', () => { - const wrapper = mount(); + const wrapper = shallow(, { 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 b6deeebdf76c6..20974519d604e 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(), ).toBe(true); }); + it('renders a TableLoader', () => { - const wrapper = mount(); + const wrapper = shallow(); 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 a2ebf36771030..7192c3ed26add 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(, { 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(), - ).toBe(true); + it('renders a Loading initially', () => { + const wrapper = setup(); + expect(wrapper.find(Loading)).toHaveLength(1); }); - it('renders', () => { - const wrapper = mount(); - 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 fafc84e9a990f..f479d229c1598 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 e81a123515aba..4c2ae814b66fc 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 (
@@ -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 3f51ee92b109e..2f57ab80d19d4 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 ; } - 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 c04e2b42250f5..d012e3499a194 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 491fcf5318896..879f7e8f8a68e 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 13146ec225306..3ad5bad015897 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 = () => ( - + + + ); export default hot(module)(Application); diff --git a/superset/assets/src/profile/components/RecentActivity.jsx b/superset/assets/src/profile/components/RecentActivity.jsx index 0a36fdaaa227b..3698680e6f245 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 64c1ec61351d9..9bfc84fc4f560 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 ( ( - + + + ); export default hot(module)(App); diff --git a/superset/assets/src/welcome/DashboardTable.jsx b/superset/assets/src/welcome/DashboardTable.jsx index 0f4c744151464..29c2988763023 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 ( ); } + return ; } } DashboardTable.propTypes = propTypes; + +export default withToasts(DashboardTable);