diff --git a/superset/assets/spec/javascripts/datasource/DatasourceEditor_spec.jsx b/superset/assets/spec/javascripts/datasource/DatasourceEditor_spec.jsx index 7c8984e378..4808c48482 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 = ; 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 7c9ed6db6d..0cc1829c46 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 = ; 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/spec/javascripts/explore/components/DatasourceControl_spec.jsx b/superset/assets/spec/javascripts/explore/components/DatasourceControl_spec.jsx index ab70ea8826..9919d36a15 100644 --- a/superset/assets/spec/javascripts/explore/components/DatasourceControl_spec.jsx +++ b/superset/assets/spec/javascripts/explore/components/DatasourceControl_spec.jsx @@ -28,7 +28,7 @@ describe('DatasourceControl', () => { function setup() { const mockStore = configureStore([]); const store = mockStore({}); - return shallow(, { context: { store } }).dive(); + return shallow(, { context: { store } }); } it('renders a Modal', () => { diff --git a/superset/assets/src/datasource/DatasourceEditor.jsx b/superset/assets/src/datasource/DatasourceEditor.jsx index 5ffb7a6448..71a2971443 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 }) { ); } + CollectionTabTitle.propTypes = { title: PropTypes.string, collection: PropTypes.array, @@ -159,6 +160,7 @@ function StackedField({ label, formElement }) { ); } + StackedField.propTypes = { label: PropTypes.string, formElement: PropTypes.node, @@ -171,6 +173,7 @@ function FormContainer({ children }) { ); } + 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,22 @@ export class DatasourceEditor extends React.PureComponent { } } syncMetadata() { - const datasource = this.state.datasource; - const url = `/datasource/external_metadata/${datasource.type}/${datasource.id}/`; + const { datasource } = this.state; this.setState({ metadataLoading: true }); - const success = (data) => { - this.mergeColumns(data); + + 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; - } + }).catch((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, }); } + findDuplicates(arr, accessor) { const seen = {}; const dups = []; @@ -284,6 +288,7 @@ export class DatasourceEditor extends React.PureComponent { }); return dups; } + validate(callback) { let errors = []; let dups; @@ -305,9 +310,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 +355,7 @@ export class DatasourceEditor extends React.PureComponent { ); } + renderAdvancedFieldset() { const datasource = this.state.datasource; return ( @@ -388,6 +396,7 @@ export class DatasourceEditor extends React.PureComponent { /> ); } + renderSpatialTab() { const { datasource } = this.state; const { spatials, all_cols: allCols } = datasource; @@ -416,6 +425,7 @@ export class DatasourceEditor extends React.PureComponent { /> ); } + renderErrors() { if (this.state.errors.length > 0) { return ( @@ -425,6 +435,7 @@ export class DatasourceEditor extends React.PureComponent { } return null; } + renderMetricCollection() { return ( ); } + render() { const datasource = this.state.datasource; return ( @@ -578,6 +590,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 915010b1b0..0d4d8e217d 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,48 @@ 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, + 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 (
@@ -111,6 +111,7 @@ class DatasourceModal extends React.PureComponent {
); } + render() { return ( { - const datasources = data.map(ds => ({ - rawName: ds.name, - connection: ds.connection, - schema: ds.schema, - name: ( - - {ds.name} - - ), - type: ds.type, - })); - that.setState({ loading: false, datasources }); - }, - error() { - that.setState({ loading: false }); - this.props.addDangerToast(t('Something went wrong while fetching the datasource list')); - }, - }); - } - } - setSearchRef(searchRef) { - this.searchRef = searchRef; - } toggleShowDatasource() { - this.setState({ showDatasource: !this.state.showDatasource }); + this.setState(({ showDatasource }) => ({ showDatasource: !showDatasource })); } + toggleModal() { - this.setState({ showModal: !this.state.showModal }); - } - selectDatasource(datasourceId) { - this.setState({ showModal: false }); - this.props.onChange(datasourceId); + this.setState(({ showModal }) => ({ showModal: !showModal })); } toggleEditDatasourceModal() { - this.setState({ showEditDatasourceModal: !this.state.showEditDatasourceModal }); - } - renderModal() { + this.setState(({ showEditDatasourceModal }) => ({ + showEditDatasourceModal: !showEditDatasourceModal, + })); } + renderDatasource() { const datasource = this.props.datasource; return ( @@ -136,6 +92,7 @@ class DatasourceControl extends React.PureComponent { ); } + render() { return (
@@ -193,4 +150,4 @@ class DatasourceControl extends React.PureComponent { DatasourceControl.propTypes = propTypes; DatasourceControl.defaultProps = defaultProps; -export default withToasts(DatasourceControl); +export default DatasourceControl;