diff --git a/superset/assets/javascripts/addSlice/AddSliceContainer.jsx b/superset/assets/javascripts/addSlice/AddSliceContainer.jsx new file mode 100644 index 0000000000..2263e20e8d --- /dev/null +++ b/superset/assets/javascripts/addSlice/AddSliceContainer.jsx @@ -0,0 +1,97 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Panel, Grid, Row, Col } from 'react-bootstrap'; +import Select from 'react-virtualized-select'; +import visTypes from '../explore/stores/visTypes'; + +const propTypes = { + datasources: PropTypes.arrayOf(PropTypes.shape({ + label: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + })).isRequired, +}; + +export default class AddSliceContainer extends React.PureComponent { + constructor(props) { + super(props); + const visTypeKeys = Object.keys(visTypes); + this.vizTypeOptions = visTypeKeys.map(vt => ({ label: visTypes[vt].label, value: vt })); + this.state = { + datasourceValue: this.props.datasources[0].value, + datasourceId: this.props.datasources[0].value.split('__')[0], + datasourceType: this.props.datasources[0].value.split('__')[1], + visType: 'table', + }; + } + + exploreUrl() { + const baseUrl = `/superset/explore/${this.state.datasourceType}/${this.state.datasourceId}`; + const formData = encodeURIComponent(JSON.stringify({ viz_type: this.state.visType })); + return `${baseUrl}?form_data=${formData}`; + } + + gotoSlice() { + window.location.href = this.exploreUrl(); + } + + changeDatasource(e) { + this.setState({ + datasourceValue: e.value, + datasourceId: e.value.split('__')[0], + datasourceType: e.value.split('__')[1], + }); + } + + changeSliceName(e) { + this.setState({ sliceName: e.target.value }); + } + + changeVisType(e) { + this.setState({ visType: e.value }); + } + + render() { + return ( +
+ Create a new slice}> + + + +
+

Choose a datasource

+ +
+
+ +

+ +
+
+
+
+ ); + } +} + +AddSliceContainer.propTypes = propTypes; diff --git a/superset/assets/javascripts/addSlice/index.jsx b/superset/assets/javascripts/addSlice/index.jsx new file mode 100644 index 0000000000..f83c2d5272 --- /dev/null +++ b/superset/assets/javascripts/addSlice/index.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { appSetup } from '../common'; +import AddSliceContainer from './AddSliceContainer'; + +appSetup(); + +const addSliceContainer = document.getElementById('js-add-slice-container'); +const bootstrapData = JSON.parse(addSliceContainer.getAttribute('data-bootstrap')); + +ReactDOM.render( + , + addSliceContainer, +); diff --git a/superset/assets/spec/javascripts/addSlice/AddSliceContainer_spec.jsx b/superset/assets/spec/javascripts/addSlice/AddSliceContainer_spec.jsx new file mode 100644 index 0000000000..9096ba3819 --- /dev/null +++ b/superset/assets/spec/javascripts/addSlice/AddSliceContainer_spec.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { expect } from 'chai'; +import { describe, it, beforeEach } from 'mocha'; +import { shallow } from 'enzyme'; +import { Button } from 'react-bootstrap'; +import Select from 'react-virtualized-select'; +import AddSliceContainer from '../../../javascripts/addSlice/AddSliceContainer'; + +const defaultProps = { + datasources: [ + { label: 'my first table', value: '1__table' }, + { label: 'another great table', value: '2__table' }, + ], +}; + +describe('AddSliceContainer', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('uses table as default visType', () => { + expect(wrapper.state().visType).to.equal('table'); + }); + + it('renders 2 selects', () => { + expect(wrapper.find(Select)).to.have.lengthOf(2); + }); + + it('renders a button', () => { + expect(wrapper.find(Button)).to.have.lengthOf(1); + }); + + it('formats explore url', () => { + const formattedUrl = '/superset/explore/table/1?form_data=%7B%22viz_type%22%3A%22table%22%7D'; + expect(wrapper.instance().exploreUrl()).to.equal(formattedUrl); + }); +}); diff --git a/superset/assets/webpack.config.js b/superset/assets/webpack.config.js index 73bcb0ace5..2f51c8c515 100644 --- a/superset/assets/webpack.config.js +++ b/superset/assets/webpack.config.js @@ -14,6 +14,7 @@ const config = { entry: { 'css-theme': APP_DIR + '/javascripts/css-theme.js', common: APP_DIR + '/javascripts/common.js', + addSlice: ['babel-polyfill', APP_DIR + '/javascripts/addSlice/index.jsx'], dashboard: ['babel-polyfill', APP_DIR + '/javascripts/dashboard/Dashboard.jsx'], explore: ['babel-polyfill', APP_DIR + '/javascripts/explore/index.jsx'], sqllab: ['babel-polyfill', APP_DIR + '/javascripts/SqlLab/index.jsx'], diff --git a/superset/templates/superset/add_slice.html b/superset/templates/superset/add_slice.html new file mode 100644 index 0000000000..c2e6978eb5 --- /dev/null +++ b/superset/templates/superset/add_slice.html @@ -0,0 +1,19 @@ +{% extends "superset/basic.html" %} + +{% block title %} + Add new slice +{% endblock %} + +{% block body %} +
+{% endblock %} + +{% block tail_js %} + {{ super() }} + {% with filename="addSlice" %} + {% include "superset/partials/_script_tag.html" %} + {% endwith %} +{% endblock %} diff --git a/superset/views/core.py b/superset/views/core.py index 802b891988..109e6b6850 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -372,11 +372,17 @@ class SliceModelView(SupersetModelView, DeleteMixin): # noqa @expose('/add', methods=['GET', 'POST']) @has_access def add(self): - flash(__( - "To create a new slice, you can open a data source " - "through the `Sources` menu, or alter an existing slice " - "from the `Slices` menu"), "info") - return redirect('/superset/welcome') + datasources = ConnectorRegistry.get_all_datasources(db.session) + datasources = [ + {'value': str(d.id) + '__' + d.type, 'label': repr(d)} + for d in datasources + ] + return self.render_template( + "superset/add_slice.html", + bootstrap_data=json.dumps({ + 'datasources': datasources, + }), + ) appbuilder.add_view( SliceModelView,