[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
This commit is contained in:
Chris Williams 2018-10-19 11:41:42 -07:00 committed by GitHub
parent 96228adda9
commit a71e6eb0a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 307 additions and 154 deletions

View File

@ -1,14 +1,22 @@
import React from 'react'; import React from 'react';
import Select from 'react-select'; import Select from 'react-select';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import sinon from 'sinon'; import fetchMock from 'fetch-mock';
import AsyncSelect from '../../../src/components/AsyncSelect'; import AsyncSelect from '../../../src/components/AsyncSelect';
describe('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 = { const mockedProps = {
dataEndpoint: '/chart/api/read', dataEndpoint,
onChange: sinon.spy(), onChange: () => {},
placeholder: 'Select...', placeholder: 'Select...',
mutator: () => [ mutator: () => [
{ value: 1, label: 'main' }, { value: 1, label: 'main' },
@ -16,6 +24,7 @@ describe('AsyncSelect', () => {
], ],
valueRenderer: opt => opt.label, valueRenderer: opt => opt.label,
}; };
it('is valid element', () => { it('is valid element', () => {
expect( expect(
React.isValidElement(<AsyncSelect {...mockedProps} />), React.isValidElement(<AsyncSelect {...mockedProps} />),
@ -30,52 +39,81 @@ describe('AsyncSelect', () => {
}); });
it('calls onChange on select change', () => { it('calls onChange on select change', () => {
const onChangeSpy = jest.fn();
const wrapper = shallow( const wrapper = shallow(
<AsyncSelect {...mockedProps} />, <AsyncSelect {...mockedProps} onChange={onChangeSpy} />,
); );
wrapper.find(Select).simulate('change', { value: 1 }); wrapper.find(Select).simulate('change', { value: 1 });
expect(mockedProps.onChange).toHaveProperty('callCount', 1); expect(onChangeSpy.mock.calls).toHaveLength(1);
}); });
describe('auto select', () => { describe('auto select', () => {
let server; it('should not call onChange if autoSelect=false', (done) => {
beforeEach(() => { expect.assertions(2);
server = sinon.fakeServer.create();
server.respondWith([
200, { 'Content-Type': 'application/json' }, JSON.stringify({}),
]);
});
afterEach(() => {
server.restore();
});
it('should be off by default', () => {
const wrapper = shallow(
<AsyncSelect {...mockedProps} />,
);
wrapper.instance().fetchOptions();
const spy = sinon.spy(wrapper.instance(), 'onChange');
expect(spy.callCount).toBe(0);
});
it('should auto select first option', () => {
const wrapper = shallow(
<AsyncSelect {...mockedProps} autoSelect />,
);
const spy = sinon.spy(wrapper.instance(), 'onChange');
server.respond();
expect(spy.callCount).toBe(1); const onChangeSpy = jest.fn();
expect(spy.calledWith(wrapper.instance().state.options[0])).toBe(true); shallow(
}); <AsyncSelect {...mockedProps} onChange={onChangeSpy} />,
it('should not auto select when value prop is set', () => {
const wrapper = shallow(
<AsyncSelect {...mockedProps} value={2} autoSelect />,
); );
const spy = sinon.spy(wrapper.instance(), 'onChange');
wrapper.instance().fetchOptions();
server.respond();
expect(spy.callCount).toBe(0); setTimeout(() => {
expect(fetchMock.calls(dataGlob)).toHaveLength(1);
expect(onChangeSpy.mock.calls).toHaveLength(0);
done();
});
});
it('should auto select the first option if autoSelect=true', (done) => {
expect.assertions(3);
const onChangeSpy = jest.fn();
const wrapper = shallow(
<AsyncSelect {...mockedProps} onChange={onChangeSpy} autoSelect />,
);
setTimeout(() => {
expect(fetchMock.calls(dataGlob)).toHaveLength(1);
expect(onChangeSpy.mock.calls).toHaveLength(1);
expect(onChangeSpy).toBeCalledWith(wrapper.instance().state.options[0]);
done();
});
});
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} value={2} onChange={onChangeSpy} autoSelect />,
);
setTimeout(() => {
expect(fetchMock.calls(dataGlob)).toHaveLength(1);
expect(onChangeSpy.mock.calls).toHaveLength(0);
expect(wrapper.find(Select)).toHaveLength(1); expect(wrapper.find(Select)).toHaveLength(1);
done();
});
});
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} />,
);
setTimeout(() => {
expect(fetchMock.calls(errorGlob)).toHaveLength(1);
expect(onAsyncError.mock.calls).toHaveLength(1);
expect(onAsyncError).toBeCalledWith('error');
done();
});
}); });
}); });
}); });

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Col, Row, Tab } from 'react-bootstrap'; import { Col, Row, Tab } from 'react-bootstrap';
import { mount } from 'enzyme'; import { shallow } from 'enzyme';
import { user } from './fixtures'; import { user } from './fixtures';
import App from '../../../src/profile/components/App'; import App from '../../../src/profile/components/App';
@ -14,13 +14,15 @@ describe('App', () => {
React.isValidElement(<App {...mockedProps} />), React.isValidElement(<App {...mockedProps} />),
).toBe(true); ).toBe(true);
}); });
it('renders 2 Col', () => { it('renders 2 Col', () => {
const wrapper = mount(<App {...mockedProps} />); const wrapper = shallow(<App {...mockedProps} />);
expect(wrapper.find(Row)).toHaveLength(1); expect(wrapper.find(Row)).toHaveLength(1);
expect(wrapper.find(Col)).toHaveLength(2); expect(wrapper.find(Col)).toHaveLength(2);
}); });
it('renders 4 Tabs', () => { it('renders 4 Tabs', () => {
const wrapper = mount(<App {...mockedProps} />); const wrapper = shallow(<App {...mockedProps} />);
expect(wrapper.find(Tab)).toHaveLength(4); expect(wrapper.find(Tab)).toHaveLength(4);
}); });
}); });

View File

@ -1,25 +1,28 @@
import React from 'react'; 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 { user } from './fixtures';
import CreatedContent from '../../../src/profile/components/CreatedContent'; import CreatedContent from '../../../src/profile/components/CreatedContent';
import TableLoader from '../../../src/components/TableLoader'; import TableLoader from '../../../src/components/TableLoader';
// store needed for withToasts(TableLoader)
const mockStore = configureStore([thunk]);
const store = mockStore({});
describe('CreatedContent', () => { describe('CreatedContent', () => {
const mockedProps = { const mockedProps = {
user, user,
}; };
it('is valid', () => {
expect(
React.isValidElement(<CreatedContent {...mockedProps} />),
).toBe(true);
});
it('renders 2 TableLoader', () => { it('renders 2 TableLoader', () => {
const wrapper = mount(<CreatedContent {...mockedProps} />); const wrapper = shallow(<CreatedContent {...mockedProps} />, { context: { store } });
expect(wrapper.find(TableLoader)).toHaveLength(2); expect(wrapper.find(TableLoader)).toHaveLength(2);
}); });
it('renders 2 titles', () => { it('renders 2 titles', () => {
const wrapper = mount(<CreatedContent {...mockedProps} />); const wrapper = shallow(<CreatedContent {...mockedProps} />, { context: { store } });
expect(wrapper.find('h3')).toHaveLength(2); expect(wrapper.find('h3')).toHaveLength(2);
}); });
}); });

View File

@ -1,25 +1,28 @@
import React from 'react'; 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 { user } from './fixtures';
import Favorites from '../../../src/profile/components/Favorites'; import Favorites from '../../../src/profile/components/Favorites';
import TableLoader from '../../../src/components/TableLoader'; import TableLoader from '../../../src/components/TableLoader';
// store needed for withToasts(TableLoader)
const mockStore = configureStore([thunk]);
const store = mockStore({});
describe('Favorites', () => { describe('Favorites', () => {
const mockedProps = { const mockedProps = {
user, user,
}; };
it('is valid', () => {
expect(
React.isValidElement(<Favorites {...mockedProps} />),
).toBe(true);
});
it('renders 2 TableLoader', () => { it('renders 2 TableLoader', () => {
const wrapper = mount(<Favorites {...mockedProps} />); const wrapper = shallow(<Favorites {...mockedProps} />, { context: { store } });
expect(wrapper.find(TableLoader)).toHaveLength(2); expect(wrapper.find(TableLoader)).toHaveLength(2);
}); });
it('renders 2 titles', () => { it('renders 2 titles', () => {
const wrapper = mount(<Favorites {...mockedProps} />); const wrapper = shallow(<Favorites {...mockedProps} />, { context: { store } });
expect(wrapper.find('h3')).toHaveLength(2); expect(wrapper.find('h3')).toHaveLength(2);
}); });
}); });

View File

@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import { mount } from 'enzyme'; import { shallow } from 'enzyme';
import { user } from './fixtures'; import { user } from './fixtures';
import RecentActivity from '../../../src/profile/components/RecentActivity'; import RecentActivity from '../../../src/profile/components/RecentActivity';
import TableLoader from '../../../src/components/TableLoader'; import TableLoader from '../../../src/components/TableLoader';
describe('RecentActivity', () => { describe('RecentActivity', () => {
const mockedProps = { const mockedProps = {
user, user,
@ -15,8 +14,9 @@ describe('RecentActivity', () => {
React.isValidElement(<RecentActivity {...mockedProps} />), React.isValidElement(<RecentActivity {...mockedProps} />),
).toBe(true); ).toBe(true);
}); });
it('renders a TableLoader', () => { it('renders a TableLoader', () => {
const wrapper = mount(<RecentActivity {...mockedProps} />); const wrapper = shallow(<RecentActivity {...mockedProps} />);
expect(wrapper.find(TableLoader)).toHaveLength(1); expect(wrapper.find(TableLoader)).toHaveLength(1);
}); });
}); });

View File

@ -1,30 +1,47 @@
import React from 'react'; import React from 'react';
import { mount } from 'enzyme'; 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 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', () => { describe('DashboardTable', () => {
const mockedProps = {}; afterEach(fetchMock.resetHistory);
let stub;
beforeEach(() => { it('renders a Loading initially', () => {
stub = sinon.stub($, 'getJSON'); const wrapper = setup();
}); expect(wrapper.find(Loading)).toHaveLength(1);
afterEach(() => {
stub.restore();
}); });
it('is valid', () => { it('fetches dashboards and renders a Table', (done) => {
expect( const wrapper = setup();
React.isValidElement(<DashboardTable {...mockedProps} />),
).toBe(true); 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();
}); });
it('renders', () => {
const wrapper = mount(<DashboardTable {...mockedProps} />);
expect(stub.callCount).toBe(1);
expect(wrapper.find('img')).toHaveLength(1);
}); });
}); });

View File

@ -21,8 +21,8 @@ $(document).ready(function () {
$('#language-picker a').click(function (ev) { $('#language-picker a').click(function (ev) {
ev.preventDefault(); ev.preventDefault();
const targetUrl = ev.currentTarget.href; SupersetClient.get({ endpoint: ev.currentTarget.href })
$.ajax(targetUrl).then(() => { .then(() => {
location.reload(); location.reload();
}); });
}); });

View File

@ -1,10 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Select from 'react-select'; import Select from 'react-select';
import { SupersetClient } from '@superset-ui/core';
import { t } from '../locales'; import { t } from '../locales';
const $ = window.$ = require('jquery');
const propTypes = { const propTypes = {
dataEndpoint: PropTypes.string.isRequired, dataEndpoint: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
@ -32,31 +31,38 @@ class AsyncSelect extends React.PureComponent {
isLoading: false, isLoading: false,
options: [], options: [],
}; };
this.onChange = this.onChange.bind(this);
} }
componentDidMount() { componentDidMount() {
this.fetchOptions(); this.fetchOptions();
} }
onChange(opt) {
this.props.onChange(opt); onChange(option) {
this.props.onChange(option);
} }
fetchOptions() { fetchOptions() {
this.setState({ isLoading: true }); this.setState({ isLoading: true });
const mutator = this.props.mutator; const { mutator, dataEndpoint } = this.props;
$.get(this.props.dataEndpoint)
.done((data) => {
this.setState({ options: mutator ? mutator(data) : data, isLoading: false });
if (!this.props.value && this.props.autoSelect && this.state.options.length) { return SupersetClient.get({ endpoint: dataEndpoint })
this.onChange(this.state.options[0]); .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) => { .catch((error) => {
this.props.onAsyncError(xhr.responseText); this.props.onAsyncError(error.error || error.statusText || error);
})
.always(() => {
this.setState({ isLoading: false }); this.setState({ isLoading: false });
}); });
} }
render() { render() {
return ( return (
<div> <div>
@ -65,7 +71,7 @@ class AsyncSelect extends React.PureComponent {
options={this.state.options} options={this.state.options}
value={this.props.value} value={this.props.value}
isLoading={this.state.isLoading} isLoading={this.state.isLoading}
onChange={this.onChange.bind(this)} onChange={this.onChange}
valueRenderer={this.props.valueRenderer} valueRenderer={this.props.valueRenderer}
{...this.props} {...this.props}
/> />

View File

@ -1,7 +1,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Table, Tr, Td } from 'reactable'; 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 Loading from '../components/Loading';
import '../../stylesheets/reactable-pagination.css'; import '../../stylesheets/reactable-pagination.css';
@ -9,9 +12,10 @@ const propTypes = {
dataEndpoint: PropTypes.string.isRequired, dataEndpoint: PropTypes.string.isRequired,
mutator: PropTypes.func, mutator: PropTypes.func,
columns: PropTypes.arrayOf(PropTypes.string), columns: PropTypes.arrayOf(PropTypes.string),
addDangerToast: PropTypes.func.isRequired,
}; };
export default class TableLoader extends React.PureComponent { class TableLoader extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -19,20 +23,34 @@ export default class TableLoader extends React.PureComponent {
data: [], data: [],
}; };
} }
componentWillMount() { componentWillMount() {
$.get(this.props.dataEndpoint, (data) => { const { dataEndpoint, mutator } = this.props;
let actualData = data;
if (this.props.mutator) { SupersetClient.get({ endpoint: dataEndpoint })
actualData = this.props.mutator(data); .then(({ json }) => {
} const data = mutator ? mutator(json) : json;
this.setState({ data: actualData, isLoading: false }); this.setState({ data, isLoading: false });
})
.catch(() => {
this.setState({ isLoading: false });
this.props.addDangerToast(t('An error occurred'));
}); });
} }
render() { render() {
if (this.state.isLoading) { if (this.state.isLoading) {
return <Loading />; return <Loading />;
} }
const tableProps = Object.assign({}, this.props);
const {
addDangerToast,
addInfoToast,
addSuccessToast,
addWarningToast,
...tableProps
} = this.props;
let { columns } = this.props; let { columns } = this.props;
if (!columns && this.state.data.length > 0) { if (!columns && this.state.data.length > 0) {
columns = Object.keys(this.state.data[0]).filter(col => col[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; TableLoader.propTypes = propTypes;
export default withToasts(TableLoader);

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import $ from 'jquery'; import { SupersetClient } from '@superset-ui/core';
import { DropdownButton, MenuItem } from 'react-bootstrap'; import { DropdownButton, MenuItem } from 'react-bootstrap';
import CssEditor from './CssEditor'; import CssEditor from './CssEditor';
@ -52,13 +52,19 @@ class HeaderActionsDropdown extends React.PureComponent {
componentWillMount() { componentWillMount() {
injectCustomCss(this.state.css); injectCustomCss(this.state.css);
$.get('/csstemplateasyncmodelview/api/read', data => { SupersetClient.get({ endpoint: '/csstemplateasyncmodelview/api/read' })
const cssTemplates = data.result.map(row => ({ .then(({ json }) => {
const cssTemplates = json.result.map(row => ({
value: row.template_name, value: row.template_name,
css: row.css, css: row.css,
label: row.template_name, label: row.template_name,
})); }));
this.setState({ cssTemplates }); this.setState({ cssTemplates });
})
.catch(() => {
this.props.addDangerToast(
t('An error occurred while fetching available CSS templates'),
);
}); });
} }

View File

@ -1,6 +1,7 @@
/* eslint camelcase: 0 */ /* eslint camelcase: 0 */
import d3 from 'd3'; import d3 from 'd3';
import $ from 'jquery'; import $ from 'jquery';
import { SupersetClient } from '@superset-ui/core';
import { formatDate, UTC } from './dates'; import { formatDate, UTC } from './dates';
const siFormatter = d3.format('.3s'); const siFormatter = d3.format('.3s');
@ -119,9 +120,11 @@ function showApiMessage(resp) {
} }
export function toggleCheckbox(apiUrlPrefix, selector) { export function toggleCheckbox(apiUrlPrefix, selector) {
const apiUrl = apiUrlPrefix + $(selector)[0].checked; SupersetClient.get({ endpoint: apiUrlPrefix + $(selector)[0].checked })
$.get(apiUrl).fail(function (xhr) { .then(() => {})
const resp = xhr.responseJSON; .catch((response) => {
// @TODO utility function to read this
const resp = response.responseJSON;
if (resp && resp.message) { if (resp && resp.message) {
showApiMessage(resp); showApiMessage(resp);
} }

View File

@ -1,6 +1,12 @@
import React from 'react'; import React from 'react';
import { hot } from 'react-hot-loader'; 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 App from './components/App';
import messageToastReducer from '../messageToasts/reducers';
import { initEnhancer } from '../reduxUtils';
import { appSetup } from '../common'; import { appSetup } from '../common';
import './main.css'; import './main.css';
@ -10,8 +16,21 @@ appSetup();
const profileViewContainer = document.getElementById('app'); const profileViewContainer = document.getElementById('app');
const bootstrap = JSON.parse(profileViewContainer.getAttribute('data-bootstrap')); const bootstrap = JSON.parse(profileViewContainer.getAttribute('data-bootstrap'));
const store = createStore(
combineReducers({
messageToasts: messageToastReducer,
}),
{},
compose(
applyMiddleware(thunk),
initEnhancer(false),
),
);
const Application = () => ( const Application = () => (
<Provider store={store}>
<App user={bootstrap.user} /> <App user={bootstrap.user} />
</Provider>
); );
export default hot(module)(Application); export default hot(module)(Application);

View File

@ -33,4 +33,5 @@ export default class RecentActivity extends React.PureComponent {
); );
} }
} }
RecentActivity.propTypes = propTypes; RecentActivity.propTypes = propTypes;

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import $ from 'jquery'; import { SupersetClient } from '@superset-ui/core';
import DeckGLContainer from '../DeckGLContainer'; import DeckGLContainer from '../DeckGLContainer';
import { getExploreLongUrl } from '../../../explore/exploreUtils'; import { getExploreLongUrl } from '../../../explore/exploreUtils';
import layerGenerators from '../layers'; import layerGenerators from '../layers';
@ -47,11 +48,13 @@ class DeckMulti extends React.PureComponent {
}, },
}; };
const url = getExploreLongUrl(subsliceCopy.form_data, 'json'); SupersetClient.get({
$.get(url, (data) => { endpoint: getExploreLongUrl(subsliceCopy.form_data, 'json'),
})
.then(({ json }) => {
const layer = layerGenerators[subsliceCopy.form_data.viz_type]( const layer = layerGenerators[subsliceCopy.form_data.viz_type](
subsliceCopy.form_data, subsliceCopy.form_data,
data, json,
); );
this.setState({ this.setState({
subSlicesLayers: { subSlicesLayers: {
@ -59,7 +62,8 @@ class DeckMulti extends React.PureComponent {
[subsliceCopy.slice_id]: layer, [subsliceCopy.slice_id]: layer,
}, },
}); });
}); })
.catch(() => {});
}); });
} }
@ -67,7 +71,7 @@ class DeckMulti extends React.PureComponent {
const { payload, viewport, formData, setControlValue } = this.props; const { payload, viewport, formData, setControlValue } = this.props;
const { subSlicesLayers } = this.state; const { subSlicesLayers } = this.state;
const layers = Object.keys(subSlicesLayers).map(k => subSlicesLayers[k]); const layers = Object.values(subSlicesLayers);
return ( return (
<DeckGLContainer <DeckGLContainer

View File

@ -1,5 +1,12 @@
import React from 'react'; import React from 'react';
import { hot } from 'react-hot-loader'; 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 { appSetup } from '../common';
import Welcome from './Welcome'; import Welcome from './Welcome';
@ -9,8 +16,21 @@ const container = document.getElementById('app');
const bootstrap = JSON.parse(container.getAttribute('data-bootstrap')); const bootstrap = JSON.parse(container.getAttribute('data-bootstrap'));
const user = { ...bootstrap.user }; const user = { ...bootstrap.user };
const store = createStore(
combineReducers({
messageToasts: messageToastReducer,
}),
{},
compose(
applyMiddleware(thunk),
initEnhancer(false),
),
);
const App = () => ( const App = () => (
<Provider store={store}>
<Welcome user={user} /> <Welcome user={user} />
</Provider>
); );
export default hot(module)(App); export default hot(module)(App);

View File

@ -1,33 +1,40 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Table, Tr, Td, unsafe } from 'reactable'; 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 Loading from '../components/Loading';
import '../../stylesheets/reactable-pagination.css'; import '../../stylesheets/reactable-pagination.css';
const $ = window.$ = require('jquery');
const propTypes = { const propTypes = {
search: PropTypes.string, search: PropTypes.string,
addDangerToast: PropTypes.func.isRequired,
}; };
export default class DashboardTable extends React.PureComponent { class DashboardTable extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
dashboards: false, dashboards: [],
}; };
} }
componentDidMount() { componentDidMount() {
const url = ( SupersetClient.get({
'/dashboardasync/api/read' + endpoint: '/dashboardasync/api/read?_oc_DashboardModelViewAsync=changed_on&_od_DashboardModelViewAsync=desc',
'?_oc_DashboardModelViewAsync=changed_on' + })
'&_od_DashboardModelViewAsync=desc'); .then(({ json }) => {
$.getJSON(url, (data) => { this.setState({ dashboards: json.result });
this.setState({ dashboards: data.result }); })
.catch(() => {
this.props.addDangerToast(t('An error occurred while fethching Dashboards'));
}); });
} }
render() { render() {
if (this.state.dashboards) { if (this.state.dashboards.length > 0) {
return ( return (
<Table <Table
className="table" className="table"
@ -58,8 +65,11 @@ export default class DashboardTable extends React.PureComponent {
</Table> </Table>
); );
} }
return <Loading />; return <Loading />;
} }
} }
DashboardTable.propTypes = propTypes; DashboardTable.propTypes = propTypes;
export default withToasts(DashboardTable);