This is an automated email from the ASF dual-hosted git repository. ccwilliams pushed a commit to branch chris--ajax-datasource-editor in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
commit 832e61fac3338c092620d490b1d34ddc2df934e4 Author: Chris Williams <chris.willi...@airbnb.com> AuthorDate: Wed Oct 17 13:04:28 2018 -0700 [superset-client][datasource editor] replace ajax with SupersetClient --- .../datasource/DatasourceEditor_spec.jsx | 28 ++++++----- .../datasource/DatasourceModal_spec.jsx | 27 ++++++----- .../assets/src/datasource/DatasourceEditor.jsx | 55 ++++++++++++++-------- superset/assets/src/datasource/DatasourceModal.jsx | 43 +++++++++-------- 4 files changed, 89 insertions(+), 64 deletions(-) diff --git a/superset/assets/spec/javascripts/datasource/DatasourceEditor_spec.jsx b/superset/assets/spec/javascripts/datasource/DatasourceEditor_spec.jsx index 7c8984e..4808c48 100644 --- a/superset/assets/spec/javascripts/datasource/DatasourceEditor_spec.jsx +++ b/superset/assets/spec/javascripts/datasource/DatasourceEditor_spec.jsx @@ -2,8 +2,8 @@ import React from 'react'; import { Tabs } from 'react-bootstrap'; import { shallow } from 'enzyme'; import configureStore from 'redux-mock-store'; -import $ from 'jquery'; -import sinon from 'sinon'; +import fetchMock from 'fetch-mock'; +import thunk from 'redux-thunk'; import DatasourceEditor from '../../../src/datasource/DatasourceEditor'; import mockDatasource from '../../fixtures/mockDatasource'; @@ -12,8 +12,9 @@ const props = { datasource: mockDatasource['7__table'], addSuccessToast: () => {}, addDangerToast: () => {}, - onChange: sinon.spy(), + onChange: () => {}, }; + const extraColumn = { column_name: 'new_column', type: 'VARCHAR(10)', @@ -25,26 +26,23 @@ const extraColumn = { groupby: true, }; +const DATASOURCE_ENDPOINT = 'glob:*/datasource/external_metadata/*'; + describe('DatasourceEditor', () => { - const mockStore = configureStore([]); + const mockStore = configureStore([thunk]); const store = mockStore({}); + fetchMock.get(DATASOURCE_ENDPOINT, []); let wrapper; let el; - let ajaxStub; let inst; beforeEach(() => { - ajaxStub = sinon.stub($, 'ajax'); el = <DatasourceEditor {...props} />; wrapper = shallow(el, { context: { store } }).dive(); inst = wrapper.instance(); }); - afterEach(() => { - ajaxStub.restore(); - }); - it('is valid', () => { expect(React.isValidElement(el)).toBe(true); }); @@ -53,12 +51,17 @@ describe('DatasourceEditor', () => { expect(wrapper.find(Tabs)).toHaveLength(1); }); - it('makes an async request', () => { + it('makes an async request', (done) => { wrapper.setState({ activeTabKey: 2 }); const syncButton = wrapper.find('.sync-from-source'); expect(syncButton).toHaveLength(1); syncButton.simulate('click'); - expect(ajaxStub.calledOnce).toBe(true); + + setTimeout(() => { + expect(fetchMock.calls(DATASOURCE_ENDPOINT)).toHaveLength(1); + fetchMock.reset(); + done(); + }, 0); }); it('merges columns', () => { @@ -67,5 +70,4 @@ describe('DatasourceEditor', () => { inst.mergeColumns([extraColumn]); expect(inst.state.databaseColumns).toHaveLength(numCols + 1); }); - }); diff --git a/superset/assets/spec/javascripts/datasource/DatasourceModal_spec.jsx b/superset/assets/spec/javascripts/datasource/DatasourceModal_spec.jsx index 7c9ed6d..0cc1829 100644 --- a/superset/assets/spec/javascripts/datasource/DatasourceModal_spec.jsx +++ b/superset/assets/spec/javascripts/datasource/DatasourceModal_spec.jsx @@ -2,7 +2,8 @@ import React from 'react'; import { Modal } from 'react-bootstrap'; import configureStore from 'redux-mock-store'; import { shallow } from 'enzyme'; -import $ from 'jquery'; +import fetchMock from 'fetch-mock'; +import thunk from 'redux-thunk'; import sinon from 'sinon'; import DatasourceModal from '../../../src/datasource/DatasourceModal'; @@ -13,31 +14,30 @@ const props = { datasource: mockDatasource['7__table'], addSuccessToast: () => {}, addDangerToast: () => {}, - onChange: sinon.spy(), + onChange: () => {}, show: true, onHide: () => {}, + onDatasourceSave: sinon.spy(), }; +const SAVE_ENDPOINT = 'glob:*/datasource/save/'; +const SAVE_PAYLOAD = { new: 'data' }; + describe('DatasourceModal', () => { - const mockStore = configureStore([]); + const mockStore = configureStore([thunk]); const store = mockStore({}); + fetchMock.post(SAVE_ENDPOINT, SAVE_PAYLOAD); let wrapper; let el; - let ajaxStub; let inst; beforeEach(() => { - ajaxStub = sinon.stub($, 'ajax'); el = <DatasourceModal {...props} />; wrapper = shallow(el, { context: { store } }).dive(); inst = wrapper.instance(); }); - afterEach(() => { - ajaxStub.restore(); - }); - it('is valid', () => { expect(React.isValidElement(el)).toBe(true); }); @@ -50,8 +50,13 @@ describe('DatasourceModal', () => { expect(wrapper.find(DatasourceEditor)).toHaveLength(1); }); - it('saves on confirm', () => { + it('saves on confirm', (done) => { inst.onConfirmSave(); - expect(ajaxStub.calledOnce).toBe(true); + setTimeout(() => { + expect(fetchMock.calls(SAVE_ENDPOINT)).toHaveLength(1); + expect(props.onDatasourceSave.getCall(0).args[0]).toEqual(SAVE_PAYLOAD); + fetchMock.reset(); + done(); + }, 0); }); }); diff --git a/superset/assets/src/datasource/DatasourceEditor.jsx b/superset/assets/src/datasource/DatasourceEditor.jsx index 5ffb7a6..86289a3 100644 --- a/superset/assets/src/datasource/DatasourceEditor.jsx +++ b/superset/assets/src/datasource/DatasourceEditor.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Alert, Badge, Col, Label, Tabs, Tab, Well } from 'react-bootstrap'; import shortid from 'shortid'; -import $ from 'jquery'; +import { SupersetClient } from '@superset-ui/core'; import { t } from '../locales'; @@ -34,6 +34,7 @@ function CollectionTabTitle({ title, collection }) { </div> ); } + CollectionTabTitle.propTypes = { title: PropTypes.string, collection: PropTypes.array, @@ -159,6 +160,7 @@ function StackedField({ label, formElement }) { </div> ); } + StackedField.propTypes = { label: PropTypes.string, formElement: PropTypes.node, @@ -171,6 +173,7 @@ function FormContainer({ children }) { </Well> ); } + FormContainer.propTypes = { children: PropTypes.node, }; @@ -181,9 +184,11 @@ const propTypes = { addSuccessToast: PropTypes.func.isRequired, addDangerToast: PropTypes.func.isRequired, }; + const defaultProps = { onChange: () => {}, }; + export class DatasourceEditor extends React.PureComponent { constructor(props) { super(props); @@ -206,6 +211,7 @@ export class DatasourceEditor extends React.PureComponent { this.validateAndChange = this.validateAndChange.bind(this); this.handleTabSelect = this.handleTabSelect.bind(this); } + onChange() { const datasource = { ...this.state.datasource, @@ -213,19 +219,24 @@ export class DatasourceEditor extends React.PureComponent { }; this.props.onChange(datasource, this.state.errors); } + onDatasourceChange(newDatasource) { this.setState({ datasource: newDatasource }, this.validateAndChange); } + onDatasourcePropChange(attr, value) { const datasource = { ...this.state.datasource, [attr]: value }; this.setState({ datasource }, this.onDatasourceChange(datasource)); } + setColumns(obj) { this.setState(obj, this.validateAndChange); } + validateAndChange() { this.validate(this.onChange); } + mergeColumns(cols) { let { databaseColumns } = this.state; let hasChanged; @@ -248,29 +259,23 @@ export class DatasourceEditor extends React.PureComponent { } } syncMetadata() { - const datasource = this.state.datasource; - const url = `/datasource/external_metadata/${datasource.type}/${datasource.id}/`; - this.setState({ metadataLoading: true }); - const success = (data) => { - this.mergeColumns(data); + const { datasource } = this.state; + this.setState(() => ({ metadataLoading: true })); + + SupersetClient.get({ + endpoint: `/datasource/external_metadata/${datasource.type}/${datasource.id}/`, + }).then(({ json }) => { + this.mergeColumns(json); this.props.addSuccessToast(t('Metadata has been synced')); - this.setState({ metadataLoading: false }); - }; - const error = (err) => { - let msg = t('An error has occurred'); - if (err.responseJSON && err.responseJSON.error) { - msg = err.responseJSON.error; - } + this.setState(() => ({ metadataLoading: false })); + }).catch((error) => { + // @TODO replace this with a util function to read body and pull error + const msg = error.error || error.statusText || t('An error has occurred'); this.props.addDangerToast(msg); - this.setState({ metadataLoading: false }); - }; - $.ajax({ - url, - type: 'GET', - success, - error, + this.setState(() => ({ metadataLoading: false })); }); } + findDuplicates(arr, accessor) { const seen = {}; const dups = []; @@ -284,6 +289,7 @@ export class DatasourceEditor extends React.PureComponent { }); return dups; } + validate(callback) { let errors = []; let dups; @@ -305,9 +311,11 @@ export class DatasourceEditor extends React.PureComponent { this.setState({ errors }, callback); } + handleTabSelect(activeTabKey) { this.setState({ activeTabKey }); } + renderSettingsFieldset() { const datasource = this.state.datasource; return ( @@ -348,6 +356,7 @@ export class DatasourceEditor extends React.PureComponent { </Fieldset> ); } + renderAdvancedFieldset() { const datasource = this.state.datasource; return ( @@ -388,6 +397,7 @@ export class DatasourceEditor extends React.PureComponent { /> </Fieldset>); } + renderSpatialTab() { const { datasource } = this.state; const { spatials, all_cols: allCols } = datasource; @@ -416,6 +426,7 @@ export class DatasourceEditor extends React.PureComponent { /> </Tab>); } + renderErrors() { if (this.state.errors.length > 0) { return ( @@ -425,6 +436,7 @@ export class DatasourceEditor extends React.PureComponent { } return null; } + renderMetricCollection() { return ( <CollectionTable @@ -490,6 +502,7 @@ export class DatasourceEditor extends React.PureComponent { allowDeletes />); } + render() { const datasource = this.state.datasource; return ( @@ -578,6 +591,8 @@ export class DatasourceEditor extends React.PureComponent { ); } } + DatasourceEditor.defaultProps = defaultProps; DatasourceEditor.propTypes = propTypes; + export default withToasts(DatasourceEditor); diff --git a/superset/assets/src/datasource/DatasourceModal.jsx b/superset/assets/src/datasource/DatasourceModal.jsx index 915010b..b895f25 100644 --- a/superset/assets/src/datasource/DatasourceModal.jsx +++ b/superset/assets/src/datasource/DatasourceModal.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Alert, Button, Modal } from 'react-bootstrap'; import Dialog from 'react-bootstrap-dialog'; -import $ from 'jquery'; +import { SupersetClient } from '@superset-ui/core'; import { t } from '../locales'; import DatasourceEditor from '../datasource/DatasourceEditor'; @@ -40,6 +40,7 @@ class DatasourceModal extends React.PureComponent { this.onConfirmSave = this.onConfirmSave.bind(this); this.setDialogRef = this.setDialogRef.bind(this); } + onClickSave() { this.dialog.show({ title: t('Confirm save'), @@ -51,49 +52,49 @@ class DatasourceModal extends React.PureComponent { body: this.renderSaveDialog(), }); } + onConfirmSave() { - const url = '/datasource/save/'; - const that = this; - $.ajax({ - url, - type: 'POST', - data: { - data: JSON.stringify(this.state.datasource), + SupersetClient.post({ + endpoint: '/datasource/save/', + postPayload: { + data: this.state.datasource, }, - success: (data) => { + }) + .then(({ json }) => { this.props.addSuccessToast(t('The datasource has been saved')); - this.props.onDatasourceSave(data); + this.props.onDatasourceSave(json); this.props.onHide(); - }, - error(err) { - let msg = t('An error has occurred'); - if (err.responseJSON && err.responseJSON.error) { - msg = err.responseJSON.error; - } - that.dialog.show({ + }) + .catch((error) => { + this.dialog.show({ title: 'Error', bsSize: 'medium', bsStyle: 'danger', actions: [ Dialog.DefaultAction('Ok', () => {}, 'btn-danger'), ], - body: msg, + // @TODO replace this with a util function to read body and pull error + body: error.error || error.statusText || t('An error has occurred'), }); - }, - }); + }); } + onDatasourceChange(datasource, errors) { this.setState({ datasource, errors }); } + setSearchRef(searchRef) { this.searchRef = searchRef; } + setDialogRef(ref) { this.dialog = ref; } + toggleShowDatasource() { this.setState({ showDatasource: !this.state.showDatasource }); } + renderSaveDialog() { return ( <div> @@ -111,6 +112,7 @@ class DatasourceModal extends React.PureComponent { </div> ); } + render() { return ( <Modal @@ -156,4 +158,5 @@ class DatasourceModal extends React.PureComponent { DatasourceModal.propTypes = propTypes; DatasourceModal.defaultProps = defaultProps; + export default withToasts(DatasourceModal);