diff --git a/superset/assets/javascripts/SqlLab/actions.js b/superset/assets/javascripts/SqlLab/actions.js
index bc8d1d0ae0..1a52939b27 100644
--- a/superset/assets/javascripts/SqlLab/actions.js
+++ b/superset/assets/javascripts/SqlLab/actions.js
@@ -37,6 +37,10 @@ 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';
+export const CREATE_DATASOURCE_FAILED = 'CREATE_DATASOURCE_FAILED';
+
export function resetState() {
return { type: RESET_STATE };
}
@@ -382,3 +386,37 @@ export function popSavedQuery(saveQueryId) {
});
};
}
+
+export function createDatasourceStarted() {
+ return { type: CREATE_DATASOURCE_STARTED };
+}
+export function createDatasourceSuccess(response) {
+ const data = JSON.parse(response);
+ const datasource = `${data.table_id}__table`;
+ return { type: CREATE_DATASOURCE_SUCCESS, datasource };
+}
+export function createDatasourceFailed(err) {
+ return { type: CREATE_DATASOURCE_FAILED, err };
+}
+
+export function createDatasource(vizOptions, context) {
+ return (dispatch) => {
+ dispatch(createDatasourceStarted());
+
+ return $.ajax({
+ type: 'POST',
+ url: '/superset/sqllab_viz/',
+ data: {
+ data: JSON.stringify(vizOptions),
+ },
+ context,
+ dataType: 'json',
+ success: (resp) => {
+ dispatch(createDatasourceSuccess(resp));
+ },
+ error: () => {
+ dispatch(createDatasourceFailed('An error occurred while creating the data source'));
+ },
+ });
+ };
+}
diff --git a/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx b/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx
index 7a4edf4c35..821baa99ae 100644
--- a/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx
+++ b/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx
@@ -1,13 +1,15 @@
/* global notify */
import React from 'react';
import PropTypes from 'prop-types';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
import { Alert, Button, Col, Modal } from 'react-bootstrap';
import Select from 'react-select';
import { Table } from 'reactable';
import shortid from 'shortid';
-import $ from 'jquery';
import { getExploreUrl } from '../../explorev2/exploreUtils';
+import * as actions from '../actions';
const CHART_TYPES = [
{ value: 'dist_bar', label: 'Distribution - Bar Chart', requiresTime: false },
@@ -17,9 +19,12 @@ const CHART_TYPES = [
];
const propTypes = {
+ actions: PropTypes.object.isRequired,
onHide: PropTypes.func,
query: PropTypes.object,
show: PropTypes.bool,
+ datasource: PropTypes.string,
+ errorMessage: PropTypes.string,
};
const defaultProps = {
show: false,
@@ -121,22 +126,14 @@ class VisualizeModal extends React.PureComponent {
sql: this.props.query.sql,
dbId: this.props.query.dbId,
};
- notify.info('Creating a data source and popping a new tab');
- $.ajax({
- type: 'POST',
- url: '/superset/sqllab_viz/',
- async: false,
- data: {
- data: JSON.stringify(vizOptions),
- },
- dataType: 'json',
- success: (resp) => {
+
+ this.props.actions.createDatasource(vizOptions, this)
+ .done(() => {
const columns = Object.keys(this.state.columns).map(k => this.state.columns[k]);
- const data = JSON.parse(resp);
const mainMetric = columns.filter(d => d.agg)[0];
const mainGroupBy = columns.filter(d => d.is_dim)[0];
const formData = {
- datasource: `${data.table_id}__table`,
+ datasource: this.props.datasource,
viz_type: this.state.chartType.value,
since: '100 years ago',
limit: '0',
@@ -148,14 +145,16 @@ class VisualizeModal extends React.PureComponent {
if (mainGroupBy) {
formData.groupby = [mainGroupBy.name];
}
+ notify.info('Creating a data source and popping a new tab');
+
window.open(getExploreUrl(formData));
- },
- error: () => notify('An error occurred while creating the data source'),
- });
+ })
+ .fail(() => {
+ notify.error(this.props.errorMessage);
+ });
}
changeDatasourceName(event) {
- this.setState({ datasourceName: event.target.value });
- this.validate();
+ this.setState({ datasourceName: event.target.value }, this.validate);
}
changeCheckbox(attr, columnName, event) {
let columns = this.mergedColumns();
@@ -271,4 +270,19 @@ class VisualizeModal extends React.PureComponent {
VisualizeModal.propTypes = propTypes;
VisualizeModal.defaultProps = defaultProps;
-export default VisualizeModal;
+function mapStateToProps(state) {
+ return {
+ datasource: state.datasource,
+ errorMessage: state.errorMessage,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators(actions, dispatch),
+ };
+}
+
+export { VisualizeModal };
+export default connect(mapStateToProps, mapDispatchToProps)(VisualizeModal);
+
diff --git a/superset/assets/javascripts/SqlLab/reducers.js b/superset/assets/javascripts/SqlLab/reducers.js
index 4b721aecba..2f5461ab8b 100644
--- a/superset/assets/javascripts/SqlLab/reducers.js
+++ b/superset/assets/javascripts/SqlLab/reducers.js
@@ -251,6 +251,25 @@ export const sqlLabReducer = function (state, action) {
}
return Object.assign({}, state, { queries: newQueries, queriesLastUpdate });
},
+ [actions.CREATE_DATASOURCE_STARTED]() {
+ return Object.assign({}, state, {
+ isDatasourceLoading: true,
+ errorMessage: null,
+ });
+ },
+ [actions.CREATE_DATASOURCE_SUCCESS]() {
+ return Object.assign({}, state, {
+ isDatasourceLoading: false,
+ errorMessage: null,
+ datasource: action.datasource,
+ });
+ },
+ [actions.CREATE_DATASOURCE_FAILED]() {
+ return Object.assign({}, state, {
+ isDatasourceLoading: false,
+ errorMessage: action.err,
+ });
+ },
};
if (action.type in actionHandlers) {
return actionHandlers[action.type]();
diff --git a/superset/assets/package.json b/superset/assets/package.json
index cc7bc605c1..17e2510ed1 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -126,6 +126,7 @@
"mocha": "^3.2.0",
"react-addons-test-utils": "^15.5.1",
"react-test-renderer": "^15.5.1",
+ "redux-mock-store": "^1.2.3",
"sinon": "^2.1.0",
"style-loader": "^0.16.1",
"transform-loader": "^0.2.3",
diff --git a/superset/assets/spec/javascripts/sqllab/QueryTable_spec.jsx b/superset/assets/spec/javascripts/sqllab/QueryTable_spec.jsx
index 3400d85b9b..7c593b76ee 100644
--- a/superset/assets/spec/javascripts/sqllab/QueryTable_spec.jsx
+++ b/superset/assets/spec/javascripts/sqllab/QueryTable_spec.jsx
@@ -1,12 +1,12 @@
import React from 'react';
-import { mount } from 'enzyme';
+import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
+import { Table } from 'reactable';
import { queries } from './fixtures';
import QueryTable from '../../../javascripts/SqlLab/components/QueryTable';
-
describe('QueryTable', () => {
const mockedProps = {
queries,
@@ -20,8 +20,9 @@ describe('QueryTable', () => {
).to.equal(true);
});
it('renders a proper table', () => {
- const wrapper = mount();
- expect(wrapper.find('table')).to.have.length(1);
- expect(wrapper.find('tr')).to.have.length(4);
+ const wrapper = shallow();
+ expect(wrapper.find(Table)).to.have.length(1);
+ expect(wrapper.find(Table).shallow().find('table')).to.have.length(1);
+ expect(wrapper.find(Table).shallow().find('table').find('Tr')).to.have.length(2);
});
});
diff --git a/superset/assets/spec/javascripts/sqllab/VisualizeModal_spec.jsx b/superset/assets/spec/javascripts/sqllab/VisualizeModal_spec.jsx
index c23529d36c..75b9929b5d 100644
--- a/superset/assets/spec/javascripts/sqllab/VisualizeModal_spec.jsx
+++ b/superset/assets/spec/javascripts/sqllab/VisualizeModal_spec.jsx
@@ -1,17 +1,58 @@
import React from 'react';
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+
import { Modal } from 'react-bootstrap';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
+import sinon from 'sinon';
+import $ from 'jquery';
import { queries } from './fixtures';
+import { sqlLabReducer } from '../../../javascripts/SqlLab/reducers';
import VisualizeModal from '../../../javascripts/SqlLab/components/VisualizeModal';
+import * as exploreUtils from '../../../javascripts/explorev2/exploreUtils';
+
+global.notify = {
+ info: () => {},
+ error: () => {},
+};
describe('VisualizeModal', () => {
+ const middlewares = [thunk];
+ const mockStore = configureStore(middlewares);
+ const initialState = sqlLabReducer(undefined, {});
+ const store = mockStore(initialState);
const mockedProps = {
show: true,
query: queries[0],
};
+ const mockColumns = {
+ ds: {
+ is_date: true,
+ is_dim: false,
+ name: 'ds',
+ type: 'STRING',
+ },
+ gender: {
+ is_date: false,
+ is_dim: true,
+ name: 'gender',
+ type: 'STRING',
+ },
+ };
+ const mockChartTypeBarChart = {
+ label: 'Distribution - Bar Chart',
+ requiresTime: false,
+ value: 'dist_bar',
+ };
+
+ const getVisualizeModalWrapper = () => (
+ shallow(, {
+ context: { store },
+ }).dive());
+
it('renders', () => {
expect(React.isValidElement()).to.equal(true);
});
@@ -21,7 +62,51 @@ describe('VisualizeModal', () => {
).to.equal(true);
});
it('renders a Modal', () => {
- const wrapper = shallow();
+ const wrapper = getVisualizeModalWrapper();
expect(wrapper.find(Modal)).to.have.length(1);
});
+
+ describe('visualize', () => {
+ const wrapper = getVisualizeModalWrapper();
+
+ wrapper.setState({
+ chartType: mockChartTypeBarChart,
+ columns: mockColumns,
+ datasourceName: 'mockDatasourceName',
+ });
+
+ const vizOptions = {
+ chartType: wrapper.state().chartType.value,
+ datasourceName: wrapper.state().datasourceName,
+ columns: wrapper.state().columns,
+ sql: wrapper.instance().props.query.sql,
+ dbId: wrapper.instance().props.query.dbId,
+ };
+
+ let spy;
+ let server;
+
+ beforeEach(() => {
+ spy = sinon.spy($, 'ajax');
+ server = sinon.fakeServer.create();
+ sinon.stub(JSON, 'parse').callsFake(() => ({ table_id: 107 }));
+ sinon.stub(exploreUtils, 'getExploreUrl').callsFake(() => ('mockURL'));
+ });
+ afterEach(() => {
+ spy.restore();
+ server.restore();
+ JSON.parse.restore();
+ exploreUtils.getExploreUrl.restore();
+ });
+
+ it('should build request', () => {
+ wrapper.instance().visualize();
+ expect(spy.callCount).to.equal(1);
+
+ const spyCall = spy.getCall(0);
+ expect(spyCall.args[0].type).to.equal('POST');
+ expect(spyCall.args[0].url).to.equal('/superset/sqllab_viz/');
+ expect(spyCall.args[0].data.data).to.equal(JSON.stringify(vizOptions));
+ });
+ });
});