+ );
+ }
+ });
+ return (
+
- )
- );
- return (
-
-
Filters
-
- {filters}
-
-
-
);
}
}
@@ -114,14 +68,9 @@ Filters.defaultProps = defaultProps;
function mapStateToProps(state) {
return {
filterColumnOpts: state.filterColumnOpts,
- filters: state.filters,
- };
-}
-
-function mapDispatchToProps(dispatch) {
- return {
- actions: bindActionCreators(actions, dispatch),
+ filters: state.viz.form_data.filters,
};
}
-export default connect(mapStateToProps, mapDispatchToProps)(Filters);
+export { Filters };
+export default connect(mapStateToProps, () => ({}))(Filters);
diff --git a/superset/assets/javascripts/explorev2/components/SaveModal.js b/superset/assets/javascripts/explorev2/components/SaveModal.js
index 08b960122db08..6c38b2f4237ef 100644
--- a/superset/assets/javascripts/explorev2/components/SaveModal.js
+++ b/superset/assets/javascripts/explorev2/components/SaveModal.js
@@ -4,6 +4,7 @@ import $ from 'jquery';
import { Modal, Alert, Button, Radio } from 'react-bootstrap';
import Select from 'react-select';
import { connect } from 'react-redux';
+import { getParamObject } from '../../modules/utils.js';
const propTypes = {
can_edit: PropTypes.bool,
@@ -57,10 +58,8 @@ class SaveModal extends React.Component {
saveOrOverwrite(gotodash) {
this.setState({ alert: null });
this.props.actions.removeSaveModalAlert();
- const params = {};
+ const params = getParamObject(this.props.form_data, this.props.datasource_type);
const sliceParams = {};
- params.datasource_id = this.props.form_data.datasource;
- params.datasource_type = this.props.datasource_type;
params.datasource_name = this.props.form_data.datasource_name;
let sliceName = null;
@@ -76,12 +75,6 @@ class SaveModal extends React.Component {
sliceParams.slice_name = this.props.form_data.slice_name;
}
- Object.keys(this.props.form_data).forEach((field) => {
- if (this.props.form_data[field] !== null && field !== 'slice_name') {
- params[field] = this.props.form_data[field];
- }
- });
-
const addToDash = this.state.addToDash;
sliceParams.add_to_dash = addToDash;
let dashboard = null;
@@ -105,7 +98,6 @@ class SaveModal extends React.Component {
default:
dashboard = null;
}
- params.V2 = true;
sliceParams.goto_dash = gotodash;
const baseUrl = '/superset/explore/' +
`${this.props.datasource_type}/${this.props.form_data.datasource}/`;
diff --git a/superset/assets/javascripts/explorev2/index.jsx b/superset/assets/javascripts/explorev2/index.jsx
index 9819e18301f63..17a37831e578a 100644
--- a/superset/assets/javascripts/explorev2/index.jsx
+++ b/superset/assets/javascripts/explorev2/index.jsx
@@ -1,3 +1,4 @@
+/* eslint camelcase: 0 */
import React from 'react';
import ReactDOM from 'react-dom';
import ExploreViewContainer from './components/ExploreViewContainer';
@@ -28,6 +29,35 @@ const bootstrappedState = Object.assign(initialState(bootstrapData.viz.form_data
bootstrappedState.viz.form_data.datasource = parseInt(bootstrapData.datasource_id, 10);
bootstrappedState.viz.form_data.datasource_name = bootstrapData.datasource_name;
+function parseFilters(form_data, prefix = 'flt') {
+ const filters = [];
+ for (let i = 0; i < 10; i++) {
+ if (form_data[`${prefix}_col_${i}`] && form_data[`${prefix}_op_${i}`]) {
+ filters.push({
+ prefix,
+ col: form_data[`${prefix}_col_${i}`],
+ op: form_data[`${prefix}_op_${i}`],
+ value: form_data[`${prefix}_eq_${i}`],
+ });
+ }
+ /* eslint no-param-reassign: 0 */
+ delete form_data[`${prefix}_col_${i}`];
+ delete form_data[`${prefix}_op_${i}`];
+ delete form_data[`${prefix}_eq_${i}`];
+ }
+ return filters;
+}
+
+function getFilters(form_data, datasource_type) {
+ if (datasource_type === 'table') {
+ return parseFilters(form_data);
+ }
+ return parseFilters(form_data).concat(parseFilters(form_data, 'having'));
+}
+
+bootstrappedState.viz.form_data.filters =
+ getFilters(bootstrappedState.viz.form_data, bootstrapData.datasource_type);
+
const store = createStore(exploreReducer, bootstrappedState,
compose(applyMiddleware(thunk))
);
diff --git a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js
index 9b4d0edf7dfe3..cb57b1834e594 100644
--- a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js
+++ b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js
@@ -43,9 +43,12 @@ export const exploreReducer = function (state, action) {
const fieldNames = Object.keys(optionsByFieldName);
fieldNames.forEach((fieldName) => {
- newState.fields[fieldName].choices = optionsByFieldName[fieldName];
+ if (fieldName === 'filterable_cols') {
+ newState.filterColumnOpts = optionsByFieldName[fieldName];
+ } else {
+ newState.fields[fieldName].choices = optionsByFieldName[fieldName];
+ }
});
-
return Object.assign({}, state, newState);
},
@@ -53,19 +56,32 @@ export const exploreReducer = function (state, action) {
return Object.assign({}, state, { filterColumnOpts: action.filterColumnOpts });
},
[actions.ADD_FILTER]() {
- return addToArr(state, 'filters', action.filter);
+ const newFormData = addToArr(state.viz.form_data, 'filters', action.filter);
+ const newState = Object.assign(
+ {},
+ state,
+ { viz: Object.assign({}, state.viz, { form_data: newFormData }) }
+ );
+ return newState;
},
[actions.REMOVE_FILTER]() {
- return removeFromArr(state, 'filters', action.filter);
- },
- [actions.CHANGE_FILTER_FIELD]() {
- return alterInArr(state, 'filters', action.filter, { field: action.field });
- },
- [actions.CHANGE_FILTER_OP]() {
- return alterInArr(state, 'filters', action.filter, { op: action.op });
+ const newFormData = removeFromArr(state.viz.form_data, 'filters', action.filter);
+ return Object.assign(
+ {},
+ state,
+ { viz: Object.assign({}, state.viz, { form_data: newFormData }) }
+ );
},
- [actions.CHANGE_FILTER_VALUE]() {
- return alterInArr(state, 'filters', action.filter, { value: action.value });
+ [actions.CHANGE_FILTER]() {
+ const changes = {};
+ changes[action.field] = action.value;
+ const newFormData = alterInArr(
+ state.viz.form_data, 'filters', action.filter, changes);
+ return Object.assign(
+ {},
+ state,
+ { viz: Object.assign({}, state.viz, { form_data: newFormData }) }
+ );
},
[actions.SET_FIELD_VALUE]() {
const newFormData = action.key === 'datasource' ?
diff --git a/superset/assets/javascripts/explorev2/stores/store.js b/superset/assets/javascripts/explorev2/stores/store.js
index 632b64fb563f7..cd70eff5291f4 100644
--- a/superset/assets/javascripts/explorev2/stores/store.js
+++ b/superset/assets/javascripts/explorev2/stores/store.js
@@ -105,6 +105,20 @@ export const commonControlPanelSections = {
],
},
],
+ filters: [
+ {
+ label: 'Filters',
+ description: 'Filters are defined using comma delimited strings as in
' +
+ 'Leave the value field empty to filter empty strings or nulls' +
+ 'For filters with comma in values, wrap them in single quotes' +
+ "as in ",
+ },
+ {
+ label: 'Result Filters',
+ description: 'The filters to apply after post-aggregation.' +
+ 'Leave the value field empty to filter empty strings or nulls',
+ },
+ ],
};
export const visTypes = {
@@ -1688,6 +1702,7 @@ export function defaultFormData(vizType = 'table', datasourceType = 'table') {
slice_name: null,
slice_id: null,
datasource_name: null,
+ filters: [],
};
const sections = sectionsToRender(vizType, datasourceType);
sections.forEach((section) => {
@@ -1722,6 +1737,7 @@ export function initialState(vizType = 'table') {
isDatasourceMetaLoading: false,
datasources: null,
datasource_type: null,
+ filterColumnOpts: [],
fields,
viz: defaultViz(vizType),
isStarred: false,
diff --git a/superset/assets/javascripts/modules/utils.js b/superset/assets/javascripts/modules/utils.js
index 157aa0ea4f50b..98e4306647de8 100644
--- a/superset/assets/javascripts/modules/utils.js
+++ b/superset/assets/javascripts/modules/utils.js
@@ -1,3 +1,4 @@
+/* eslint camelcase: 0 */
const d3 = require('d3');
const $ = require('jquery');
@@ -152,3 +153,35 @@ export function slugify(string) {
.replace(/[\s\W-]+/g, '-') // replace spaces, non-word chars, w/ a single dash (-)
.replace(/-$/, ''); // remove last floating dash
}
+
+function formatFilters(filters) {
+ // outputs an object of url params of filters
+ // prefix can be 'flt' or 'having'
+ const params = {};
+ for (let i = 0; i < filters.length; i++) {
+ const filter = filters[i];
+ params[`${filter.prefix}_col_${i + 1}`] = filter.col;
+ params[`${filter.prefix}_op_${i + 1}`] = filter.op;
+ params[`${filter.prefix}_eq_${i + 1}`] = filter.value;
+ }
+ return params;
+}
+
+export function getParamObject(form_data, datasource_type) {
+ const data = {
+ // V2 tag temporarily for updating url
+ // Todo: remove after launch
+ V2: true,
+ datasource_id: form_data.datasource,
+ datasource_type,
+ };
+ Object.keys(form_data).forEach((field) => {
+ // filter out null fields
+ if (form_data[field] !== null && field !== 'datasource') {
+ data[field] = form_data[field];
+ }
+ });
+ const filterParams = formatFilters(form_data.filters);
+ Object.assign(data, filterParams);
+ return data;
+}
diff --git a/superset/assets/spec/javascripts/explorev2/actions_spec.js b/superset/assets/spec/javascripts/explorev2/actions_spec.js
index ceef46223884f..0b4cb2d6c0397 100644
--- a/superset/assets/spec/javascripts/explorev2/actions_spec.js
+++ b/superset/assets/spec/javascripts/explorev2/actions_spec.js
@@ -15,4 +15,15 @@ describe('reducers', () => {
actions.setFieldValue('table', 'show_legend'));
expect(newState.viz.form_data.show_legend).to.equal(false);
});
+ it('adds a filter given a new filter', () => {
+ const newState = exploreReducer(initialState('table'),
+ actions.addFilter({
+ id: 1,
+ prefix: 'flt',
+ col: null,
+ op: null,
+ value: null,
+ }));
+ expect(newState.viz.form_data.filters).to.have.length(1);
+ });
});
diff --git a/superset/assets/spec/javascripts/explorev2/components/Filter_spec.js b/superset/assets/spec/javascripts/explorev2/components/Filter_spec.js
new file mode 100644
index 0000000000000..9a55b59cdc51d
--- /dev/null
+++ b/superset/assets/spec/javascripts/explorev2/components/Filter_spec.js
@@ -0,0 +1,41 @@
+/* eslint-disable no-unused-expressions */
+import React from 'react';
+import Select from 'react-select';
+import { Button } from 'react-bootstrap';
+import { expect } from 'chai';
+import { describe, it, beforeEach } from 'mocha';
+import { shallow } from 'enzyme';
+import Filter from '../../../../javascripts/explorev2/components/Filter';
+
+const defaultProps = {
+ actions: {},
+ filterColumnOpts: ['country_name'],
+ filter: {
+ id: 1,
+ prefix: 'flt',
+ col: 'country_name',
+ eq: 'in',
+ value: 'China',
+ },
+ prefix: 'flt',
+};
+
+describe('Filter', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallow();
+ });
+
+ it('renders Filters', () => {
+ expect(
+ React.isValidElement()
+ ).to.equal(true);
+ });
+
+ it('renders two select, one button and one input', () => {
+ expect(wrapper.find(Select)).to.have.lengthOf(2);
+ expect(wrapper.find(Button)).to.have.lengthOf(1);
+ expect(wrapper.find('input')).to.have.lengthOf(1);
+ });
+});
diff --git a/superset/assets/spec/javascripts/explorev2/components/Filters_spec.js b/superset/assets/spec/javascripts/explorev2/components/Filters_spec.js
new file mode 100644
index 0000000000000..4074bc96e127e
--- /dev/null
+++ b/superset/assets/spec/javascripts/explorev2/components/Filters_spec.js
@@ -0,0 +1,40 @@
+/* eslint-disable no-unused-expressions */
+import React from 'react';
+import { Button } from 'react-bootstrap';
+import { expect } from 'chai';
+import { describe, it, beforeEach } from 'mocha';
+import { shallow } from 'enzyme';
+import { Filters } from '../../../../javascripts/explorev2/components/Filters';
+import Filter from '../../../../javascripts/explorev2/components/Filter';
+
+const defaultProps = {
+ filterColumnOpts: ['country_name'],
+ filters: [
+ {
+ id: 1,
+ prefix: 'flt',
+ col: 'country_name',
+ eq: 'in',
+ value: 'China',
+ }],
+ prefix: 'flt',
+};
+
+describe('Filters', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallow();
+ });
+
+ it('renders Filters', () => {
+ expect(
+ React.isValidElement()
+ ).to.equal(true);
+ });
+
+ it('renders one filter', () => {
+ expect(wrapper.find(Filter)).to.have.lengthOf(1);
+ expect(wrapper.find(Button)).to.have.lengthOf(1);
+ });
+});
diff --git a/superset/views.py b/superset/views.py
index 277e070933512..5614209bbcf0d 100755
--- a/superset/views.py
+++ b/superset/views.py
@@ -2401,6 +2401,7 @@ def fetch_datasource_metadata(self):
'size': datasource.metrics_combo,
'mapbox_label': all_cols,
'point_radius': [(c, c) for c in (["Auto"] + datasource.column_names)],
+ 'filterable_cols': datasource.filterable_column_names,
}
return Response(
diff --git a/tests/core_tests.py b/tests/core_tests.py
index c6e13153da6fe..d5de2cc31ca7c 100644
--- a/tests/core_tests.py
+++ b/tests/core_tests.py
@@ -430,7 +430,7 @@ def test_fetch_datasource_metadata(self):
url = '/superset/fetch_datasource_metadata?datasource_type=table&' \
'datasource_id=1'
resp = json.loads(self.get_resp(url))
- self.assertEqual(len(resp['field_options']), 20)
+ self.assertEqual(len(resp['field_options']), 21)
def test_fetch_all_tables(self):
self.login(username='admin')