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);

Reply via email to