mirror of https://github.com/apache/superset.git
[superset-client][datasource editor] replace ajax with SupersetClient (#6134)
* [superset-client][datasource editor] replace ajax with SupersetClient * [superset-client][datasource control] replace ajax with SupersetClient * [superset-client][datasource editor] remove unused funcs in DatasourceControl * [superset-client][data source control] lint, remove toasts * [superset-client] fix DatasourceControl_spec * [superset-client] remove unneeded functional setState calls
This commit is contained in:
parent
546d150b91
commit
96228adda9
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('DatasourceControl', () => {
|
|||
function setup() {
|
||||
const mockStore = configureStore([]);
|
||||
const store = mockStore({});
|
||||
return shallow(<DatasourceControl {...defaultProps} />, { context: { store } }).dive();
|
||||
return shallow(<DatasourceControl {...defaultProps} />, { context: { store } });
|
||||
}
|
||||
|
||||
it('renders a Modal', () => {
|
||||
|
|
|
@ -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,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 {
|
|||
</Fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
renderAdvancedFieldset() {
|
||||
const datasource = this.state.datasource;
|
||||
return (
|
||||
|
@ -388,6 +396,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||
/>
|
||||
</Fieldset>);
|
||||
}
|
||||
|
||||
renderSpatialTab() {
|
||||
const { datasource } = this.state;
|
||||
const { spatials, all_cols: allCols } = datasource;
|
||||
|
@ -416,6 +425,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||
/>
|
||||
</Tab>);
|
||||
}
|
||||
|
||||
renderErrors() {
|
||||
if (this.state.errors.length > 0) {
|
||||
return (
|
||||
|
@ -425,6 +435,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderMetricCollection() {
|
||||
return (
|
||||
<CollectionTable
|
||||
|
@ -490,6 +501,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||
allowDeletes
|
||||
/>);
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
|
@ -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 (
|
||||
<div>
|
||||
|
@ -111,6 +111,7 @@ class DatasourceModal extends React.PureComponent {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
|
@ -156,4 +157,5 @@ class DatasourceModal extends React.PureComponent {
|
|||
|
||||
DatasourceModal.propTypes = propTypes;
|
||||
DatasourceModal.defaultProps = defaultProps;
|
||||
|
||||
export default withToasts(DatasourceModal);
|
||||
|
|
|
@ -9,20 +9,16 @@ import {
|
|||
Tooltip,
|
||||
Well,
|
||||
} from 'react-bootstrap';
|
||||
import $ from 'jquery';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import { t } from '../../../locales';
|
||||
import DatasourceModal from '../../../datasource/DatasourceModal';
|
||||
import ColumnOption from '../../../components/ColumnOption';
|
||||
import MetricOption from '../../../components/MetricOption';
|
||||
import withToasts from '../../../messageToasts/enhancers/withToasts';
|
||||
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string.isRequired,
|
||||
addDangerToast: PropTypes.func.isRequired,
|
||||
datasource: PropTypes.object.isRequired,
|
||||
onDatasourceSave: PropTypes.func,
|
||||
};
|
||||
|
@ -39,70 +35,30 @@ class DatasourceControl extends React.PureComponent {
|
|||
showEditDatasourceModal: false,
|
||||
loading: true,
|
||||
showDatasource: false,
|
||||
datasources: null,
|
||||
};
|
||||
this.toggleShowDatasource = this.toggleShowDatasource.bind(this);
|
||||
this.toggleEditDatasourceModal = this.toggleEditDatasourceModal.bind(this);
|
||||
this.setSearchRef = this.setSearchRef.bind(this);
|
||||
this.selectDatasource = this.selectDatasource.bind(this);
|
||||
}
|
||||
|
||||
onChange(vizType) {
|
||||
this.props.onChange(vizType);
|
||||
this.setState({ showModal: false });
|
||||
}
|
||||
onEnterModal() {
|
||||
if (this.searchRef) {
|
||||
this.searchRef.focus();
|
||||
}
|
||||
const url = '/superset/datasources/';
|
||||
const that = this;
|
||||
if (!this.state.datasources) {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url,
|
||||
success: (data) => {
|
||||
const datasources = data.map(ds => ({
|
||||
rawName: ds.name,
|
||||
connection: ds.connection,
|
||||
schema: ds.schema,
|
||||
name: (
|
||||
<a
|
||||
href="#"
|
||||
onClick={this.selectDatasource.bind(this, ds.uid)}
|
||||
className="datasource-link"
|
||||
>
|
||||
{ds.name}
|
||||
</a>
|
||||
),
|
||||
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 {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
|
@ -193,4 +150,4 @@ class DatasourceControl extends React.PureComponent {
|
|||
DatasourceControl.propTypes = propTypes;
|
||||
DatasourceControl.defaultProps = defaultProps;
|
||||
|
||||
export default withToasts(DatasourceControl);
|
||||
export default DatasourceControl;
|
||||
|
|
Loading…
Reference in New Issue