Get query button working in explorev2 (#1581)

* Get query buttonw working in explorev2

 - Create new endpoint for updating explore viz
 - Send over new form_data when query button is pressed

* Added endpoint test

* Changes based on comments

* Added docstring for endpoint, and query spec

* Remove white space around docstring
This commit is contained in:
vera-liu 2016-11-16 13:21:53 -08:00 committed by GitHub
parent ed3d44d591
commit 83d08b8b8f
11 changed files with 224 additions and 36 deletions

View File

@ -13,7 +13,7 @@ export default function QueryAndSaveBtns({ canAdd, onQuery }) {
return (
<div className="btn-group query-and-save">
<button type="button" className="btn btn-primary btn-sm" onClick={onQuery}>
<button id="query_button" type="button" className="btn btn-primary btn-sm" onClick={onQuery}>
<i className="fa fa-bolt"></i> Query
</button>
<button

View File

@ -1,26 +1,17 @@
/* eslint camelcase: 0 */
const $ = window.$ = require('jquery');
export const SET_DATASOURCE = 'SET_DATASOURCE';
export const SET_FIELD_OPTIONS = 'SET_FIELD_OPTIONS';
export const TOGGLE_SEARCHBOX = 'TOGGLE_SEARCHBOX';
export const SET_FILTER_COLUMN_OPTS = 'SET_FILTER_COLUMN_OPTS';
export const ADD_FILTER = 'ADD_FILTER';
export const SET_FILTER = 'SET_FILTER';
export const REMOVE_FILTER = 'REMOVE_FILTER';
export const CHANGE_FILTER_FIELD = 'CHANGE_FILTER_FIELD';
export const CHANGE_FILTER_OP = 'CHANGE_FILTER_OP';
export const CHANGE_FILTER_VALUE = 'CHANGE_FILTER_VALUE';
export const CLEAR_ALL_OPTS = 'CLEAR_ALL_OPTS';
export const SET_DATASOURCE_TYPE = 'SET_DATASOURCE_TYPE';
export const SET_FIELD_VALUE = 'SET_FIELD_VALUE';
export const SET_FIELD_OPTIONS = 'SET_FIELD_OPTIONS';
export function setFieldOptions(options) {
return { type: SET_FIELD_OPTIONS, options };
}
export const CLEAR_ALL_OPTS = 'CLEAR_ALL_OPTS';
export function clearAllOpts() {
return { type: CLEAR_ALL_OPTS };
}
export const SET_DATASOURCE_TYPE = 'SET_DATASOURCE_TYPE';
export function setDatasourceType(datasourceType) {
return { type: SET_DATASOURCE_TYPE, datasourceType };
}
@ -62,26 +53,71 @@ export function fetchFieldOptions(datasourceId, datasourceType) {
}
};
}
export const ADD_FILTER = 'ADD_FILTER';
export function addFilter(filter) {
return { type: ADD_FILTER, filter };
}
export const REMOVE_FILTER = 'REMOVE_FILTER';
export function removeFilter(filter) {
return { type: REMOVE_FILTER, filter };
}
export const CHANGE_FILTER_FIELD = 'CHANGE_FILTER_FIELD';
export function changeFilterField(filter, field) {
return { type: CHANGE_FILTER_FIELD, filter, field };
}
export const CHANGE_FILTER_OP = 'CHANGE_FILTER_OP';
export function changeFilterOp(filter, op) {
return { type: CHANGE_FILTER_OP, filter, op };
}
export const CHANGE_FILTER_VALUE = 'CHANGE_FILTER_VALUE';
export function changeFilterValue(filter, value) {
return { type: CHANGE_FILTER_VALUE, filter, value };
}
export const SET_FIELD_VALUE = 'SET_FIELD_VALUE';
export function setFieldValue(key, value) {
return { type: SET_FIELD_VALUE, key, value };
}
export const UPDATE_CHART = 'UPDATE_CHART';
export function updateChart(viz) {
return { type: UPDATE_CHART, viz };
}
export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
export function chartUpdateStarted() {
return { type: CHART_UPDATE_STARTED };
}
export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED ';
export function chartUpdateFailed() {
return { type: CHART_UPDATE_FAILED };
}
export function updateExplore(datasource_type, datasource_id, form_data) {
return function (dispatch) {
dispatch(chartUpdateStarted);
const updateUrl =
`/superset/update_explore/${datasource_type}/${datasource_id}/`;
$.ajax({
type: 'POST',
url: updateUrl,
data: {
data: JSON.stringify(form_data),
},
success: (data) => {
dispatch(updateChart(JSON.parse(data)));
},
error(error) {
dispatch(chartUpdateFailed(error));
},
});
};
}

View File

@ -17,6 +17,8 @@ const propTypes = {
standalone_endpoint: PropTypes.string.isRequired,
query: PropTypes.string.isRequired,
column_formats: PropTypes.object,
data: PropTypes.any,
isChartLoading: PropTypes.bool,
};
class ChartContainer extends React.Component {
@ -29,30 +31,34 @@ class ChartContainer extends React.Component {
}
componentWillMount() {
this.setState({ mockSlice: this.getMockedSliceObject() });
this.setState({ mockSlice: this.getMockedSliceObject(this.props) });
}
componentDidMount() {
this.renderVis();
}
componentWillReceiveProps(nextProps) {
this.setState({ mockSlice: this.getMockedSliceObject(nextProps) });
}
componentDidUpdate() {
this.renderVis();
}
getMockedSliceObject() {
getMockedSliceObject(props) {
return {
viewSqlQuery: this.props.query,
viewSqlQuery: props.query,
data: {
csv_endpoint: this.props.csv_endpoint,
json_endpoint: this.props.json_endpoint,
standalone_endpoint: this.props.standalone_endpoint,
csv_endpoint: props.csv_endpoint,
json_endpoint: props.json_endpoint,
standalone_endpoint: props.standalone_endpoint,
},
containerId: this.props.containerId,
containerId: props.containerId,
jsonEndpoint: () => this.props.json_endpoint,
jsonEndpoint: () => props.json_endpoint,
container: {
html: (data) => {
@ -66,7 +72,7 @@ class ChartContainer extends React.Component {
// should call callback to adjust height of chart
$(this.state.selector).css(dim, size);
},
height: () => parseInt(this.props.height, 10) - 100,
height: () => parseInt(props.height, 10) - 100,
show: () => { this.render(); },
@ -78,7 +84,7 @@ class ChartContainer extends React.Component {
width: () => this.chartContainerRef.getBoundingClientRect().width,
height: () => parseInt(this.props.height, 10) - 100,
height: () => parseInt(props.height, 10) - 100,
selector: this.state.selector,
@ -128,6 +134,7 @@ class ChartContainer extends React.Component {
};
}
renderVis() {
visMap[this.props.viz_type](this.state.mockSlice).render();
}
@ -152,11 +159,13 @@ class ChartContainer extends React.Component {
</div>
}
>
<div
id={this.props.containerId}
ref={(ref) => { this.chartContainerRef = ref; }}
className={this.props.viz_type}
/>
{!this.props.isChartLoading &&
<div
id={this.props.containerId}
ref={(ref) => { this.chartContainerRef = ref; }}
className={this.props.viz_type}
/>
}
</Panel>
</div>
);
@ -176,6 +185,8 @@ function mapStateToProps(state) {
standalone_endpoint: state.viz.standalone_endpoint,
query: state.viz.query,
column_formats: state.viz.column_formats,
data: state.viz.data,
isChartLoading: state.isChartLoading,
};
}

View File

@ -1,9 +1,24 @@
/* eslint camelcase: 0 */
import React from 'react';
import { bindActionCreators } from 'redux';
import * as actions from '../actions/exploreActions';
import { connect } from 'react-redux';
import ChartContainer from './ChartContainer';
import ControlPanelsContainer from './ControlPanelsContainer';
import QueryAndSaveBtns from '../../explore/components/QueryAndSaveBtns';
const $ = require('jquery');
export default class ExploreViewContainer extends React.Component {
const propTypes = {
form_data: React.PropTypes.object.isRequired,
actions: React.PropTypes.object.isRequired,
slice_id: React.PropTypes.string.isRequired,
slice_name: React.PropTypes.string.isRequired,
datasource_id: React.PropTypes.number.isRequired,
datasource_type: React.PropTypes.string.isRequired,
};
class ExploreViewContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
@ -11,11 +26,43 @@ export default class ExploreViewContainer extends React.Component {
};
}
onQuery() {
const data = {};
const form_data = this.props.form_data;
Object.keys(form_data).forEach((field) => {
// filter out null fields
if (form_data[field] !== null) {
data[field] = form_data[field];
}
});
// V2 tag temporarily for updating url
// Todo: remove after launch
data.V2 = true;
data.datasource_id = this.props.datasource_id;
data.datasource_type = this.props.datasource_type;
this.queryFormData(data);
const params = $.param(data, true);
this.updateUrl(params);
}
getHeight() {
const navHeight = 90;
return `${window.innerHeight - navHeight}px`;
}
updateUrl(params) {
const baseUrl =
`/superset/explore/${this.props.datasource_type}/${this.props.datasource_id}/`;
const newEndpoint = `${baseUrl}?${params}`;
history.pushState({}, document.title, newEndpoint);
}
queryFormData(data) {
this.props.actions.updateExplore(
this.props.datasource_type, this.props.datasource_id, data);
}
render() {
return (
<div
@ -29,10 +76,13 @@ export default class ExploreViewContainer extends React.Component {
<div className="col-sm-4">
<QueryAndSaveBtns
canAdd="True"
onQuery={() => {}}
onQuery={this.onQuery.bind(this)}
/>
<br /><br />
<ControlPanelsContainer />
<ControlPanelsContainer
actions={this.props.actions}
form_data={this.props.form_data}
/>
</div>
<div className="col-sm-8">
<ChartContainer
@ -44,3 +94,25 @@ export default class ExploreViewContainer extends React.Component {
);
}
}
ExploreViewContainer.propTypes = propTypes;
function mapStateToProps(state) {
return {
datasource_id: state.datasource_id,
datasource_type: state.datasource_type,
form_data: state.viz.form_data,
slice_id: state.viz.form_data.slice_id,
slice_name: state.viz.form_data.slice_name,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch),
};
}
export { ControlPanelsContainer };
export default connect(mapStateToProps, mapDispatchToProps)(ExploreViewContainer);

View File

@ -7,7 +7,7 @@ import Select, { Creatable } from 'react-select';
const propTypes = {
name: PropTypes.string.isRequired,
choices: PropTypes.array,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]).isRequired,
label: PropTypes.string,
description: PropTypes.string,
onChange: PropTypes.func,

View File

@ -19,6 +19,7 @@ const bootstrappedState = Object.assign(initialState, {
datasource_type: bootstrapData.datasource_type,
viz: bootstrapData.viz,
});
const store = createStore(exploreReducer, bootstrappedState,
compose(applyMiddleware(thunk))
);

View File

@ -68,6 +68,29 @@ export const exploreReducer = function (state, action) {
{ viz: Object.assign({}, state.viz, { form_data: newFormData }) }
);
},
[actions.UPDATE_CHART]() {
const vizUpdates = {
column_formats: action.viz.column_formats,
json_endpoint: action.viz.json_endpoint,
csv_endpoint: action.viz.csv_endpoint,
standalone_endpoint: action.viz.standalone_endpoint,
query: action.viz.query,
data: action.viz.data,
};
return Object.assign(
{},
state,
{
viz: Object.assign({}, state.viz, vizUpdates),
isChartLoading: false,
});
},
[actions.CHART_UPDATE_STARTED]() {
return Object.assign({}, state, { isChartLoading: true });
},
[actions.CHART_UPDATE_FAILED]() {
return Object.assign({}, state, { isChartLoading: false });
},
};
if (action.type in actionHandlers) {
return actionHandlers[action.type]();

View File

@ -604,7 +604,7 @@ export const visTypes = {
label: 'Heatmap',
controlPanelSections: [
{
label: null,
label: 'Axis & Metrics',
fieldSetRows: [
['all_columns_x'],
['all_columns_y'],

View File

@ -2,13 +2,14 @@ import React from 'react';
import { beforeEach, describe, it } from 'mocha';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import QueryAndSaveButtons from '../../../../javascripts/explore/components/QueryAndSaveBtns';
describe('QueryAndSaveButtons', () => {
const defaultProps = {
canAdd: 'True',
onQuery: () => {},
onQuery: sinon.spy(),
};
// It must render
@ -32,5 +33,11 @@ describe('QueryAndSaveButtons', () => {
expect(wrapper.find('button').contains(' Query')).to.eql(true);
expect(wrapper.find('button').contains(' Save as')).to.eql(true);
});
it('calls onQuery when query button is clicked', () => {
const queryButton = wrapper.find('#query_button');
queryButton.simulate('click');
expect(defaultProps.onQuery.called).to.eql(true);
});
});
});

View File

@ -1244,7 +1244,8 @@ class Superset(BaseSupersetView):
viz_type = args.get('viz_type', 'table')
datasource = SourceRegistry.get_datasource(
datasource_type, datasource_id, db.session)
viz_obj = viz.viz_types[viz_type](datasource, request.args)
viz_obj = viz.viz_types[viz_type](
datasource, request.args if request.args else args)
return viz_obj
@has_access
@ -1253,6 +1254,24 @@ class Superset(BaseSupersetView):
viz_obj = self.get_viz(slice_id)
return redirect(viz_obj.get_url(**request.args))
@log_this
@has_access_api
@expose(
"/update_explore/<datasource_type>/<datasource_id>/", methods=['POST'])
def update_explore(self, datasource_type, datasource_id):
"""Send back new viz on POST request for updating update explore view"""
form_data = json.loads(request.form.get('data'))
error_redirect = '/slicemodelview/list/'
try:
viz_obj = self.get_viz(
datasource_type=datasource_type,
datasource_id=datasource_id,
args=form_data)
except Exception as e:
flash('{}'.format(e), "alert")
return redirect(error_redirect)
return viz_obj.get_json()
@has_access_api
@expose("/explore_json/<datasource_type>/<datasource_id>/")
def explore_json(self, datasource_type, datasource_id):

View File

@ -97,6 +97,25 @@ class CoreTests(SupersetTestCase):
assert_admin_view_menus_in('Alpha', self.assertNotIn)
assert_admin_view_menus_in('Gamma', self.assertNotIn)
def test_update_explore(self):
self.login(username='admin')
tbl_id = self.table_ids.get('energy_usage')
data = json.dumps({
'viz_type': 'sankey',
'groupby': ['source', 'target'],
'metrics': ['sum__value'],
'row_limit': 5000,
'flt_col_0': 'source',
'datasource_name': 'energy_usage',
'datasource_id': tbl_id,
'datasource_type': 'table',
'previous_viz_type': 'sankey'
})
response = self.client.post('/superset/update_explore/table/{}/'.format(tbl_id),
data=dict(data=data))
assert response.status_code == 200
self.logout()
def test_save_slice(self):
self.login(username='admin')
slice_id = self.get_slice("Energy Sankey", db.session).id