Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

apply redux for VisualizeModal #2795

Merged
merged 4 commits into from
May 24, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions superset/assets/javascripts/SqlLab/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand Down Expand Up @@ -382,3 +386,38 @@ 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/',
async: false,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think we want/need to use aync: false here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data: {
data: JSON.stringify(vizOptions),
},
context,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think we need to set context here, since we are not using this in the success or error callbacks.
https://stackoverflow.com/questions/5097191/ajax-context-option#5097214

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will need 'this' in the chained done/fail

dataType: 'json',
success: (resp) => {
dispatch(createDatasourceSuccess(resp));
},
error: () => {
dispatch(createDatasourceFailed('An error occurred while creating the data source'));
},
});
};
}
52 changes: 33 additions & 19 deletions superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx
Original file line number Diff line number Diff line change
@@ -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 },
Expand All @@ -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,
Expand Down Expand Up @@ -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',
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the general approach is to minimize the number of components that interact directly with redux. Components that do should be suffixed .*Container and only act as a container/wrapper (no business or component logic, purely used to hook in parts of the state & actions).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we aren't really using a consistent pattern around this yet. currently the following components in SQLLab are connected to redux/mapping state to props:
App.jsx
DataPreviewModal.jsx
QueryAutoRefresh.jsx
SouthPane.jsx
TabbedSqlEditors.jsx

for this component, we could pass actions, datasource, and errorMessage through the component tree down from TabbedSqlEditors if we wanted to avoid connecting to the store here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About 'When to Introduce Containers?', I feel this article is reasonable: Presentational and Container Components.

For VisulizeModal component, I feel its state (datasource, and errorMessage) are rarely needed by other components. If we make it a dump component, since VisulizeModal is used by QueryTable and ResultSet, does it means both parents need to pass additional pros down? So I prefer to wrap its own states in a container.

Currently SqlLab code structure is more like:

  • components
    • component1.jsx
    • component2.jsx
  • actions_for_all_components.js
  • reducers_for_all_actions.js
    The problem is now presentation components are injected with so many actions it doesn't need. this makes sub-component hard to be reused outside SqlLab.

I will prefer to have it breakdown into

  • contaienr1
    • container1.jsx
    • component1.jsx
    • action_for_comp1.js
    • reducer_for_comp1.js
  • component2
    • component2.js
    • container2.js
    • action_for_comp2
    • reducer_for_comp2

I am still new to this code base, would like to hear your opinions. Thank you!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a good article about presentational and container components. there are a lot of good points there, and i agree that it's an ideal architecture, separating out data fetching/updating logic from pure presentational components.

i think it would be worth doing a deep dive on the the component/redux architecture of sql lab, to see where we might optimize/organize which components will be containers, and which will be presentational. right now we have a bit of a mix happening.

i would say connecting to redux from the visualizeModal component for now works, and allows us to better test that component by making that change. let's leave re-organizing for ideal container/presentation components for a follow up PR.


19 changes: 19 additions & 0 deletions superset/assets/javascripts/SqlLab/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]();
Expand Down
1 change: 1 addition & 0 deletions superset/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 6 additions & 5 deletions superset/assets/spec/javascripts/sqllab/QueryTable_spec.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react';
import { mount } from 'enzyme';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';

import { queries } from './fixtures';
import QueryTable from '../../../javascripts/SqlLab/components/QueryTable';

import { Table } from 'reactable';

describe('QueryTable', () => {
const mockedProps = {
Expand All @@ -20,8 +20,9 @@ describe('QueryTable', () => {
).to.equal(true);
});
it('renders a proper table', () => {
const wrapper = mount(<QueryTable {...mockedProps} />);
expect(wrapper.find('table')).to.have.length(1);
expect(wrapper.find('tr')).to.have.length(4);
const wrapper = shallow(<QueryTable {...mockedProps} />);
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);
});
});
85 changes: 84 additions & 1 deletion superset/assets/spec/javascripts/sqllab/VisualizeModal_spec.jsx
Original file line number Diff line number Diff line change
@@ -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(<VisualizeModal {...mockedProps} />, {
context: { store },
}).dive());

it('renders', () => {
expect(React.isValidElement(<VisualizeModal />)).to.equal(true);
});
Expand All @@ -21,7 +62,49 @@ describe('VisualizeModal', () => {
).to.equal(true);
});
it('renders a Modal', () => {
const wrapper = shallow(<VisualizeModal {...mockedProps} />);
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, 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));
});
});
});
4 changes: 4 additions & 0 deletions superset/assets/stylesheets/superset.css
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,7 @@ div.widget .slice_container {
.table-condensed {
font-size: 12px;
}

.table-condensed input[type="checkbox"] {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably scope this to the visualize modal only.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. i will move it to another PR.

float: left;
}