@@ -112,6 +128,5 @@ function mapDispatchToProps(dispatch) {
};
}
-export { ControlPanelsContainer };
-
+export { ExploreViewContainer };
export default connect(mapStateToProps, mapDispatchToProps)(ExploreViewContainer);
diff --git a/superset/assets/javascripts/explorev2/components/FieldSet.jsx b/superset/assets/javascripts/explorev2/components/FieldSet.jsx
index 03b6ded919..7097028d18 100644
--- a/superset/assets/javascripts/explorev2/components/FieldSet.jsx
+++ b/superset/assets/javascripts/explorev2/components/FieldSet.jsx
@@ -14,7 +14,11 @@ const propTypes = {
places: PropTypes.number,
validators: PropTypes.any,
onChange: React.PropTypes.func,
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.array]).isRequired,
+ value: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ PropTypes.bool,
+ PropTypes.array]).isRequired,
};
const defaultProps = {
diff --git a/superset/assets/javascripts/explorev2/components/SaveModal.js b/superset/assets/javascripts/explorev2/components/SaveModal.js
new file mode 100644
index 0000000000..08b960122d
--- /dev/null
+++ b/superset/assets/javascripts/explorev2/components/SaveModal.js
@@ -0,0 +1,241 @@
+/* eslint camel-case: 0 */
+import React, { PropTypes } from 'react';
+import $ from 'jquery';
+import { Modal, Alert, Button, Radio } from 'react-bootstrap';
+import Select from 'react-select';
+import { connect } from 'react-redux';
+
+const propTypes = {
+ can_edit: PropTypes.bool,
+ onHide: PropTypes.func.isRequired,
+ actions: PropTypes.object.isRequired,
+ form_data: PropTypes.object,
+ datasource_type: PropTypes.string.isRequired,
+ user_id: PropTypes.string.isRequired,
+ dashboards: PropTypes.array.isRequired,
+ alert: PropTypes.string,
+};
+
+class SaveModal extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ saveToDashboardId: null,
+ newDashboardName: '',
+ newSliceName: '',
+ dashboards: [],
+ alert: null,
+ action: 'overwrite',
+ addToDash: 'noSave',
+ };
+ }
+ componentDidMount() {
+ this.props.actions.fetchDashboards(this.props.user_id);
+ }
+ onChange(name, event) {
+ switch (name) {
+ case 'newSliceName':
+ this.setState({ newSliceName: event.target.value });
+ break;
+ case 'saveToDashboardId':
+ this.setState({ saveToDashboardId: event.value });
+ this.changeDash('existing');
+ break;
+ case 'newDashboardName':
+ this.setState({ newDashboardName: event.target.value });
+ break;
+ default:
+ break;
+ }
+ }
+ changeAction(action) {
+ this.setState({ action });
+ }
+ changeDash(dash) {
+ this.setState({ addToDash: dash });
+ }
+ saveOrOverwrite(gotodash) {
+ this.setState({ alert: null });
+ this.props.actions.removeSaveModalAlert();
+ const params = {};
+ 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;
+ sliceParams.action = this.state.action;
+ if (sliceParams.action === 'saveas') {
+ sliceName = this.state.newSliceName;
+ if (sliceName === '') {
+ this.setState({ alert: 'Please enter a slice name' });
+ return;
+ }
+ sliceParams.slice_name = sliceName;
+ } else {
+ 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;
+ switch (addToDash) {
+ case ('existing'):
+ dashboard = this.state.saveToDashboardId;
+ if (!dashboard) {
+ this.setState({ alert: 'Please select a dashboard' });
+ return;
+ }
+ sliceParams.save_to_dashboard_id = dashboard;
+ break;
+ case ('new'):
+ dashboard = this.state.newDashboardName;
+ if (dashboard === '') {
+ this.setState({ alert: 'Please enter a dashboard name' });
+ return;
+ }
+ sliceParams.new_dashboard_name = dashboard;
+ break;
+ default:
+ dashboard = null;
+ }
+ params.V2 = true;
+ sliceParams.goto_dash = gotodash;
+ const baseUrl = '/superset/explore/' +
+ `${this.props.datasource_type}/${this.props.form_data.datasource}/`;
+ const saveUrl = `${baseUrl}?${$.param(params, true)}&${$.param(sliceParams, true)}`;
+ this.props.actions.saveSlice(saveUrl);
+ this.props.onHide();
+ }
+ removeAlert() {
+ if (this.props.alert) {
+ this.props.actions.removeSaveModalAlert();
+ }
+ this.setState({ alert: null });
+ }
+ render() {
+ return (
+
+
+
+ Save A Slice
+
+
+
+ {(this.state.alert || this.props.alert) &&
+
+ {this.state.alert ? this.state.alert : this.props.alert}
+
+
+ }
+
+ {`Overwrite slice ${this.props.form_data.slice_name}`}
+
+
+ Save as
+
+
+
+
+
+
+
+
+ Do not add to a dashboard
+
+
+
+ Add slice to existing dashboard
+
+
+
+
+ Add to new dashboard
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+SaveModal.propTypes = propTypes;
+
+function mapStateToProps(state) {
+ return {
+ can_edit: state.can_edit,
+ user_id: state.user_id,
+ dashboards: state.dashboards,
+ alert: state.saveModalAlert,
+ };
+}
+
+export { SaveModal };
+export default connect(mapStateToProps, () => ({}))(SaveModal);
diff --git a/superset/assets/javascripts/explorev2/index.jsx b/superset/assets/javascripts/explorev2/index.jsx
index 2b7fc14583..e30b41acf8 100644
--- a/superset/assets/javascripts/explorev2/index.jsx
+++ b/superset/assets/javascripts/explorev2/index.jsx
@@ -13,10 +13,12 @@ const bootstrapData = JSON.parse(exploreViewContainer.getAttribute('data-bootstr
import { exploreReducer } from './reducers/exploreReducer';
const bootstrappedState = Object.assign(initialState(bootstrapData.viz.form_data.viz_type), {
+ can_edit: bootstrapData.can_edit,
can_download: bootstrapData.can_download,
datasources: bootstrapData.datasources,
datasource_type: bootstrapData.datasource_type,
viz: bootstrapData.viz,
+ user_id: bootstrapData.user_id,
});
bootstrappedState.viz.form_data.datasource = parseInt(bootstrapData.datasource_id, 10);
bootstrappedState.viz.form_data.datasource_name = bootstrapData.datasource_name;
diff --git a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js
index 1316c0bca2..da93cdf269 100644
--- a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js
+++ b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js
@@ -27,6 +27,16 @@ export const exploreReducer = function (state, action) {
[actions.REMOVE_CONTROL_PANEL_ALERT]() {
return Object.assign({}, state, { controlPanelAlert: null });
},
+
+ [actions.FETCH_DASHBOARDS_SUCCEEDED]() {
+ return Object.assign({}, state, { dashboards: action.choices });
+ },
+
+ [actions.FETCH_DASHBOARDS_FAILED]() {
+ return Object.assign({}, state,
+ { saveModalAlert: `fetching dashboards failed for ${action.userId}` });
+ },
+
[actions.SET_FIELD_OPTIONS]() {
const newState = Object.assign({}, state);
const optionsByFieldName = action.options;
@@ -66,6 +76,9 @@ export const exploreReducer = function (state, action) {
newFormData.slice_name = state.viz.form_data.slice_name;
newFormData.viz_type = state.viz.form_data.viz_type;
}
+ if (action.key === 'viz_type') {
+ newFormData.previous_viz_type = state.viz.form_data.viz_type;
+ }
newFormData[action.key] = action.value ? action.value : (!state.viz.form_data[action.key]);
return Object.assign(
{},
@@ -99,6 +112,12 @@ export const exploreReducer = function (state, action) {
[actions.REMOVE_CHART_ALERT]() {
return Object.assign({}, state, { chartAlert: null });
},
+ [actions.SAVE_SLICE_FAILED]() {
+ return Object.assign({}, state, { saveModalAlert: 'Failed to save slice' });
+ },
+ [actions.REMOVE_SAVE_MODAL_ALERT]() {
+ return Object.assign({}, state, { saveModalAlert: null });
+ },
};
if (action.type in actionHandlers) {
return actionHandlers[action.type]();
diff --git a/superset/assets/javascripts/explorev2/stores/store.js b/superset/assets/javascripts/explorev2/stores/store.js
index 5d86903286..4ceefe71ed 100644
--- a/superset/assets/javascripts/explorev2/stores/store.js
+++ b/superset/assets/javascripts/explorev2/stores/store.js
@@ -750,7 +750,7 @@ export const fields = {
type: 'SelectMultipleSortableField',
label: 'Metrics',
choices: [],
- default: null,
+ default: [],
description: 'One or many metrics to display',
},
@@ -758,6 +758,7 @@ export const fields = {
type: 'SelectMultipleSortableField',
label: 'Ordering',
choices: [],
+ default: [],
description: 'One or many metrics to display',
},
@@ -931,6 +932,7 @@ export const fields = {
type: 'SelectMultipleSortableField',
label: 'Group by',
choices: [],
+ default: [],
description: 'One or many fields to group by',
},
@@ -938,6 +940,7 @@ export const fields = {
type: 'SelectMultipleSortableField',
label: 'Columns',
choices: [],
+ default: [],
description: 'One or many fields to pivot as columns',
},
@@ -945,6 +948,7 @@ export const fields = {
type: 'SelectMultipleSortableField',
label: 'Columns',
choices: [],
+ default: [],
description: 'Columns to display',
},
@@ -1552,6 +1556,7 @@ export const fields = {
type: 'SelectMultipleSortableField',
label: 'label',
choices: [],
+ default: [],
description: '`count` is COUNT(*) if a group by is used. ' +
'Numerical columns will be aggregated with the aggregator. ' +
'Non-numerical columns will be used to label points. ' +
@@ -1704,6 +1709,7 @@ export function defaultViz(vizType) {
export function initialState(vizType = 'table') {
return {
+ dashboards: [],
isDatasourceMetaLoading: false,
datasources: null,
datasource_type: null,
diff --git a/superset/assets/spec/javascripts/explorev2/components/SaveModal_spec.js b/superset/assets/spec/javascripts/explorev2/components/SaveModal_spec.js
new file mode 100644
index 0000000000..dd86bdf616
--- /dev/null
+++ b/superset/assets/spec/javascripts/explorev2/components/SaveModal_spec.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import { expect } from 'chai';
+import { describe, it, beforeEach } from 'mocha';
+import { shallow } from 'enzyme';
+import { defaultFormData } from '../../../../javascripts/explorev2/stores/store';
+import { SaveModal } from '../../../../javascripts/explorev2/components/SaveModal';
+import { Modal, Button, Radio } from 'react-bootstrap';
+import sinon from 'sinon';
+
+const defaultProps = {
+ can_edit: true,
+ onHide: () => ({}),
+ actions: {
+ saveSlice: sinon.spy(),
+ },
+ form_data: defaultFormData,
+ datasource_id: 1,
+ datasource_name: 'birth_names',
+ datasource_type: 'table',
+ user_id: 1,
+};
+
+describe('SaveModal', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallow(
);
+ });
+
+ it('renders a Modal with 7 inputs and 2 buttons', () => {
+ expect(wrapper.find(Modal)).to.have.lengthOf(1);
+ expect(wrapper.find('input')).to.have.lengthOf(2);
+ expect(wrapper.find(Button)).to.have.lengthOf(2);
+ expect(wrapper.find(Radio)).to.have.lengthOf(5);
+ });
+});
diff --git a/superset/views.py b/superset/views.py
index 597d0fbdd8..6e66b9017e 100755
--- a/superset/views.py
+++ b/superset/views.py
@@ -1456,7 +1456,8 @@ class Superset(BaseSupersetView):
# TODO use form processing form wtforms
d = args.to_dict(flat=False)
del d['action']
- del d['previous_viz_type']
+ if 'previous_viz_type' in d:
+ del d['previous_viz_type']
as_list = ('metrics', 'groupby', 'columns', 'all_columns',
'mapbox_label', 'order_by_cols')
@@ -1515,8 +1516,12 @@ class Superset(BaseSupersetView):
db.session.commit()
if request.args.get('goto_dash') == 'true':
+ if request.args.get('V2') == 'true':
+ return dash.url
return redirect(dash.url)
else:
+ if request.args.get('V2') == 'true':
+ return slc.slice_url
return redirect(slc.slice_url)
def save_slice(self, slc):