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)); + }); + }); });