mirror of https://github.com/apache/superset.git
feat: update dataset editor modal (#10347)
This commit is contained in:
parent
e89e60df76
commit
39fad8575c
|
@ -17,17 +17,23 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
import { Modal } from 'react-bootstrap';
|
import { Modal } from 'react-bootstrap';
|
||||||
import configureStore from 'redux-mock-store';
|
import configureStore from 'redux-mock-store';
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
import ChangeDatasourceModal from 'src/datasource/ChangeDatasourceModal';
|
import ChangeDatasourceModal from 'src/datasource/ChangeDatasourceModal';
|
||||||
|
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||||
import mockDatasource from '../../fixtures/mockDatasource';
|
import mockDatasource from '../../fixtures/mockDatasource';
|
||||||
|
|
||||||
const props = {
|
const mockStore = configureStore([thunk]);
|
||||||
|
const store = mockStore({});
|
||||||
|
|
||||||
|
const mockedProps = {
|
||||||
addDangerToast: () => {},
|
addDangerToast: () => {},
|
||||||
onDatasourceSave: sinon.spy(),
|
onDatasourceSave: sinon.spy(),
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
|
@ -37,62 +43,53 @@ const props = {
|
||||||
|
|
||||||
const datasource = mockDatasource['7__table'];
|
const datasource = mockDatasource['7__table'];
|
||||||
const datasourceData = {
|
const datasourceData = {
|
||||||
id: datasource.name,
|
id: datasource.id,
|
||||||
type: datasource.type,
|
type: datasource.type,
|
||||||
uid: datasource.id,
|
uid: datasource.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DATASOURCES_ENDPOINT = 'glob:*/superset/datasources/';
|
const DATASOURCES_ENDPOINT = 'glob:*/superset/datasources/';
|
||||||
const DATASOURCE_ENDPOINT = `glob:*/datasource/get/${datasourceData.type}/${datasourceData.id}`;
|
const DATASOURCE_ENDPOINT = `glob:*/datasource/get/${datasourceData.type}/${datasourceData.id}`;
|
||||||
const DATASOURCES_PAYLOAD = { json: 'data' };
|
|
||||||
const DATASOURCE_PAYLOAD = { new: 'data' };
|
const DATASOURCE_PAYLOAD = { new: 'data' };
|
||||||
|
|
||||||
|
fetchMock.get(DATASOURCES_ENDPOINT, [mockDatasource['7__table']]);
|
||||||
|
fetchMock.get(DATASOURCE_ENDPOINT, DATASOURCE_PAYLOAD);
|
||||||
|
|
||||||
|
async function mountAndWait(props = mockedProps) {
|
||||||
|
const mounted = mount(<ChangeDatasourceModal {...props} />, {
|
||||||
|
context: { store },
|
||||||
|
wrappingComponent: ThemeProvider,
|
||||||
|
wrappingComponentProps: { theme: supersetTheme },
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(mounted);
|
||||||
|
|
||||||
|
return mounted;
|
||||||
|
}
|
||||||
|
|
||||||
describe('ChangeDatasourceModal', () => {
|
describe('ChangeDatasourceModal', () => {
|
||||||
const mockStore = configureStore([thunk]);
|
|
||||||
const store = mockStore({});
|
|
||||||
fetchMock.get(DATASOURCES_ENDPOINT, DATASOURCES_PAYLOAD);
|
|
||||||
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let el;
|
|
||||||
let inst;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
el = <ChangeDatasourceModal {...props} />;
|
wrapper = await mountAndWait();
|
||||||
wrapper = shallow(el, { context: { store } }).dive();
|
|
||||||
inst = wrapper.instance();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is valid', () => {
|
it('renders', () => {
|
||||||
expect(React.isValidElement(el)).toBe(true);
|
expect(wrapper.find(ChangeDatasourceModal)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a Modal', () => {
|
it('renders a Modal', () => {
|
||||||
expect(wrapper.find(Modal)).toHaveLength(1);
|
expect(wrapper.find(Modal)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetches datasources', () => {
|
it('fetches datasources', async () => {
|
||||||
return new Promise(done => {
|
expect(fetchMock.calls(/superset\/datasources/)).toHaveLength(3);
|
||||||
inst.onEnterModal();
|
|
||||||
setTimeout(() => {
|
|
||||||
expect(fetchMock.calls(DATASOURCES_ENDPOINT)).toHaveLength(1);
|
|
||||||
fetchMock.reset();
|
|
||||||
done();
|
|
||||||
}, 0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('changes the datasource', () => {
|
it('changes the datasource', async () => {
|
||||||
return new Promise(done => {
|
act(() => {
|
||||||
fetchMock.get(DATASOURCE_ENDPOINT, DATASOURCE_PAYLOAD);
|
wrapper.find('.datasource-link').at(0).props().onClick(datasourceData);
|
||||||
inst.selectDatasource(datasourceData);
|
|
||||||
setTimeout(() => {
|
|
||||||
expect(fetchMock.calls(DATASOURCE_ENDPOINT)).toHaveLength(1);
|
|
||||||
expect(props.onDatasourceSave.getCall(0).args[0]).toEqual(
|
|
||||||
DATASOURCE_PAYLOAD,
|
|
||||||
);
|
|
||||||
fetchMock.reset();
|
|
||||||
done();
|
|
||||||
}, 0);
|
|
||||||
});
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
expect(fetchMock.calls(/datasource\/get\/table\/7/)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,66 +17,80 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
import { Modal } from 'react-bootstrap';
|
import { Modal } from 'react-bootstrap';
|
||||||
import configureStore from 'redux-mock-store';
|
import configureStore from 'redux-mock-store';
|
||||||
import { shallow } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
|
||||||
|
|
||||||
|
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||||
import DatasourceModal from 'src/datasource/DatasourceModal';
|
import DatasourceModal from 'src/datasource/DatasourceModal';
|
||||||
import DatasourceEditor from 'src/datasource/DatasourceEditor';
|
import DatasourceEditor from 'src/datasource/DatasourceEditor';
|
||||||
import mockDatasource from '../../fixtures/mockDatasource';
|
import mockDatasource from '../../fixtures/mockDatasource';
|
||||||
|
|
||||||
const props = {
|
const mockStore = configureStore([thunk]);
|
||||||
datasource: mockDatasource['7__table'],
|
const store = mockStore({});
|
||||||
|
const datasource = mockDatasource['7__table'];
|
||||||
|
|
||||||
|
const SAVE_ENDPOINT = 'glob:*/api/v1/dataset/7';
|
||||||
|
const SAVE_PAYLOAD = { new: 'data' };
|
||||||
|
|
||||||
|
const mockedProps = {
|
||||||
|
datasource,
|
||||||
addSuccessToast: () => {},
|
addSuccessToast: () => {},
|
||||||
addDangerToast: () => {},
|
addDangerToast: () => {},
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
show: true,
|
|
||||||
onHide: () => {},
|
onHide: () => {},
|
||||||
|
show: true,
|
||||||
onDatasourceSave: sinon.spy(),
|
onDatasourceSave: sinon.spy(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const SAVE_ENDPOINT = 'glob:*/datasource/save/';
|
async function mountAndWait(props = mockedProps) {
|
||||||
const SAVE_PAYLOAD = { new: 'data' };
|
const mounted = mount(<DatasourceModal {...props} />, {
|
||||||
|
context: { store },
|
||||||
|
wrappingComponent: ThemeProvider,
|
||||||
|
wrappingComponentProps: { theme: supersetTheme },
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(mounted);
|
||||||
|
|
||||||
|
return mounted;
|
||||||
|
}
|
||||||
|
|
||||||
describe('DatasourceModal', () => {
|
describe('DatasourceModal', () => {
|
||||||
const mockStore = configureStore([thunk]);
|
|
||||||
const store = mockStore({});
|
|
||||||
fetchMock.post(SAVE_ENDPOINT, SAVE_PAYLOAD);
|
fetchMock.post(SAVE_ENDPOINT, SAVE_PAYLOAD);
|
||||||
|
const callsP = fetchMock.put(SAVE_ENDPOINT, SAVE_PAYLOAD);
|
||||||
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let el;
|
|
||||||
let inst;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
el = <DatasourceModal {...props} />;
|
wrapper = await mountAndWait();
|
||||||
wrapper = shallow(el, { context: { store } }).dive();
|
|
||||||
inst = wrapper.instance();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is valid', () => {
|
it('renders', () => {
|
||||||
expect(React.isValidElement(el)).toBe(true);
|
expect(wrapper.find(DatasourceModal)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a Modal', () => {
|
it('renders a Modal', () => {
|
||||||
expect(wrapper.find(Modal)).toHaveLength(1);
|
expect(wrapper.find(Modal)).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a DatasourceEditor', () => {
|
it('renders a DatasourceEditor', () => {
|
||||||
expect(wrapper.find(DatasourceEditor)).toHaveLength(1);
|
expect(wrapper.find(DatasourceEditor)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('saves on confirm', () => {
|
it('saves on confirm', async () => {
|
||||||
return new Promise(done => {
|
act(() => {
|
||||||
inst.onConfirmSave();
|
wrapper.find('[className="m-r-5"]').props().onClick();
|
||||||
setTimeout(() => {
|
|
||||||
expect(fetchMock.calls(SAVE_ENDPOINT)).toHaveLength(1);
|
|
||||||
expect(props.onDatasourceSave.getCall(0).args[0]).toEqual(SAVE_PAYLOAD);
|
|
||||||
fetchMock.reset();
|
|
||||||
done();
|
|
||||||
}, 0);
|
|
||||||
});
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
act(() => {
|
||||||
|
const okButton = wrapper.find('[className="btn btn-sm btn-primary"]');
|
||||||
|
okButton.simulate('click');
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
expect(callsP._calls).toHaveLength(2); /* eslint no-underscore-dangle: 0 */
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,179 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Table } from 'reactable-arc';
|
|
||||||
import { Alert, FormControl, Modal } from 'react-bootstrap';
|
|
||||||
import { SupersetClient } from '@superset-ui/connection';
|
|
||||||
import { t } from '@superset-ui/translation';
|
|
||||||
|
|
||||||
import getClientErrorObject from '../utils/getClientErrorObject';
|
|
||||||
import Loading from '../components/Loading';
|
|
||||||
import withToasts from '../messageToasts/enhancers/withToasts';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
addDangerToast: PropTypes.func.isRequired,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
onDatasourceSave: PropTypes.func,
|
|
||||||
onHide: PropTypes.func,
|
|
||||||
show: PropTypes.bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
onChange: () => {},
|
|
||||||
onDatasourceSave: () => {},
|
|
||||||
onHide: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const TABLE_COLUMNS = ['name', 'type', 'schema', 'connection', 'creator'];
|
|
||||||
const TABLE_FILTERABLE = ['rawName', 'type', 'schema', 'connection', 'creator'];
|
|
||||||
const CHANGE_WARNING_MSG = t(
|
|
||||||
'Changing the datasource may break the chart if the chart relies ' +
|
|
||||||
'on columns or metadata that does not exist in the target datasource',
|
|
||||||
);
|
|
||||||
|
|
||||||
class ChangeDatasourceModal extends React.PureComponent {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
loading: true,
|
|
||||||
datasources: null,
|
|
||||||
};
|
|
||||||
this.setSearchRef = this.setSearchRef.bind(this);
|
|
||||||
this.onEnterModal = this.onEnterModal.bind(this);
|
|
||||||
this.selectDatasource = this.selectDatasource.bind(this);
|
|
||||||
this.changeSearch = this.changeSearch.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
onEnterModal() {
|
|
||||||
if (this.searchRef) {
|
|
||||||
this.searchRef.focus();
|
|
||||||
}
|
|
||||||
if (!this.state.datasources) {
|
|
||||||
SupersetClient.get({
|
|
||||||
endpoint: '/superset/datasources/',
|
|
||||||
})
|
|
||||||
.then(({ json }) => {
|
|
||||||
const datasources = json.map(ds => ({
|
|
||||||
rawName: ds.name,
|
|
||||||
connection: ds.connection,
|
|
||||||
schema: ds.schema,
|
|
||||||
name: (
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
onClick={this.selectDatasource.bind(this, ds)}
|
|
||||||
className="datasource-link"
|
|
||||||
>
|
|
||||||
{ds.name}
|
|
||||||
</a>
|
|
||||||
),
|
|
||||||
type: ds.type,
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.setState({ loading: false, datasources });
|
|
||||||
})
|
|
||||||
.catch(response => {
|
|
||||||
this.setState({ loading: false });
|
|
||||||
getClientErrorObject(response).then(({ error }) => {
|
|
||||||
this.props.addDangerToast(error.error || error.statusText || error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setSearchRef(searchRef) {
|
|
||||||
this.searchRef = searchRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
changeSearch(event) {
|
|
||||||
this.setState({ filter: event.target.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
selectDatasource(datasource) {
|
|
||||||
SupersetClient.get({
|
|
||||||
endpoint: `/datasource/get/${datasource.type}/${datasource.id}`,
|
|
||||||
})
|
|
||||||
.then(({ json }) => {
|
|
||||||
this.props.onDatasourceSave(json);
|
|
||||||
this.props.onChange(datasource.uid);
|
|
||||||
})
|
|
||||||
.catch(response => {
|
|
||||||
getClientErrorObject(response).then(({ error, message }) => {
|
|
||||||
const errorMessage = error
|
|
||||||
? error.error || error.statusText || error
|
|
||||||
: message;
|
|
||||||
this.props.addDangerToast(errorMessage);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.props.onHide();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { datasources, filter, loading } = this.state;
|
|
||||||
const { show, onHide } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
show={show}
|
|
||||||
onHide={onHide}
|
|
||||||
onEnter={this.onEnterModal}
|
|
||||||
onExit={this.setSearchRef}
|
|
||||||
bsSize="lg"
|
|
||||||
>
|
|
||||||
<Modal.Header closeButton>
|
|
||||||
<Modal.Title>{t('Select a datasource')}</Modal.Title>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Body>
|
|
||||||
<Alert bsStyle="warning" showIcon>
|
|
||||||
<strong>{t('Warning!')}</strong> {CHANGE_WARNING_MSG}
|
|
||||||
</Alert>
|
|
||||||
<div>
|
|
||||||
<FormControl
|
|
||||||
inputRef={ref => {
|
|
||||||
this.setSearchRef(ref);
|
|
||||||
}}
|
|
||||||
type="text"
|
|
||||||
bsSize="sm"
|
|
||||||
value={filter}
|
|
||||||
placeholder={t('Search / Filter')}
|
|
||||||
onChange={this.changeSearch}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{loading && <Loading />}
|
|
||||||
{datasources && (
|
|
||||||
<Table
|
|
||||||
columns={TABLE_COLUMNS}
|
|
||||||
className="table table-condensed"
|
|
||||||
data={datasources}
|
|
||||||
itemsPerPage={20}
|
|
||||||
filterable={TABLE_FILTERABLE}
|
|
||||||
filterBy={filter}
|
|
||||||
hideFilterInput
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal.Body>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ChangeDatasourceModal.propTypes = propTypes;
|
|
||||||
ChangeDatasourceModal.defaultProps = defaultProps;
|
|
||||||
|
|
||||||
export default withToasts(ChangeDatasourceModal);
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React, { FunctionComponent, useState, useRef } from 'react';
|
||||||
|
// @ts-ignore
|
||||||
|
import { Table } from 'reactable-arc';
|
||||||
|
import { Alert, FormControl, Modal } from 'react-bootstrap';
|
||||||
|
import { SupersetClient } from '@superset-ui/connection';
|
||||||
|
import { t } from '@superset-ui/translation';
|
||||||
|
|
||||||
|
import getClientErrorObject from '../utils/getClientErrorObject';
|
||||||
|
import Loading from '../components/Loading';
|
||||||
|
import withToasts from '../messageToasts/enhancers/withToasts';
|
||||||
|
|
||||||
|
interface ChangeDatasourceModalProps {
|
||||||
|
addDangerToast: (msg: string) => void;
|
||||||
|
onChange: (id: number) => void;
|
||||||
|
onDatasourceSave: (datasource: object, errors?: Array<any>) => {};
|
||||||
|
onHide: () => void;
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TABLE_COLUMNS = ['name', 'type', 'schema', 'connection', 'creator'];
|
||||||
|
const TABLE_FILTERABLE = ['rawName', 'type', 'schema', 'connection', 'creator'];
|
||||||
|
const CHANGE_WARNING_MSG = t(
|
||||||
|
'Changing the datasource may break the chart if the chart relies ' +
|
||||||
|
'on columns or metadata that does not exist in the target datasource',
|
||||||
|
);
|
||||||
|
|
||||||
|
const ChangeDatasourceModal: FunctionComponent<ChangeDatasourceModalProps> = ({
|
||||||
|
addDangerToast,
|
||||||
|
onChange,
|
||||||
|
onDatasourceSave,
|
||||||
|
onHide,
|
||||||
|
show,
|
||||||
|
}) => {
|
||||||
|
const [datasources, setDatasources] = useState<any>(null);
|
||||||
|
const [filter, setFilter] = useState<any>(undefined);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
let searchRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const selectDatasource = (datasource: any) => {
|
||||||
|
SupersetClient.get({
|
||||||
|
endpoint: `/datasource/get/${datasource.type}/${datasource.id}`,
|
||||||
|
})
|
||||||
|
.then(({ json }) => {
|
||||||
|
onDatasourceSave(json);
|
||||||
|
onChange(datasource.uid);
|
||||||
|
})
|
||||||
|
.catch(response => {
|
||||||
|
getClientErrorObject(response).then(
|
||||||
|
({ error, message }: { error: any; message: string }) => {
|
||||||
|
const errorMessage = error
|
||||||
|
? error.error || error.statusText || error
|
||||||
|
: message;
|
||||||
|
addDangerToast(errorMessage);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
onHide();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEnterModal = () => {
|
||||||
|
if (searchRef && searchRef.current) {
|
||||||
|
searchRef.current.focus();
|
||||||
|
}
|
||||||
|
if (!datasources) {
|
||||||
|
SupersetClient.get({
|
||||||
|
endpoint: '/superset/datasources/',
|
||||||
|
})
|
||||||
|
.then(({ json }) => {
|
||||||
|
const data = json.map((ds: any) => ({
|
||||||
|
rawName: ds.name,
|
||||||
|
connection: ds.connection,
|
||||||
|
schema: ds.schema,
|
||||||
|
name: (
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={() => selectDatasource(ds)}
|
||||||
|
className="datasource-link"
|
||||||
|
>
|
||||||
|
{ds.name}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
type: ds.type,
|
||||||
|
}));
|
||||||
|
setLoading(false);
|
||||||
|
setDatasources(data);
|
||||||
|
})
|
||||||
|
.catch(response => {
|
||||||
|
setLoading(false);
|
||||||
|
getClientErrorObject(response).then(({ error }: any) => {
|
||||||
|
addDangerToast(error.error || error.statusText || error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSearchRef = (ref: any) => {
|
||||||
|
searchRef = ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFilter(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal show={show} onHide={onHide} onEnter={onEnterModal} bsSize="lg">
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title>{t('Select a datasource')}</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<Alert bsStyle="warning">
|
||||||
|
<strong>{t('Warning!')}</strong> {CHANGE_WARNING_MSG}
|
||||||
|
</Alert>
|
||||||
|
<div>
|
||||||
|
<FormControl
|
||||||
|
inputRef={ref => {
|
||||||
|
setSearchRef(ref);
|
||||||
|
}}
|
||||||
|
type="text"
|
||||||
|
bsSize="sm"
|
||||||
|
value={filter}
|
||||||
|
placeholder={t('Search / Filter')}
|
||||||
|
// @ts-ignore
|
||||||
|
onChange={changeSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{loading && <Loading />}
|
||||||
|
{datasources && (
|
||||||
|
<Table
|
||||||
|
columns={TABLE_COLUMNS}
|
||||||
|
className="table table-condensed"
|
||||||
|
data={datasources}
|
||||||
|
itemsPerPage={20}
|
||||||
|
filterable={TABLE_FILTERABLE}
|
||||||
|
filterBy={filter}
|
||||||
|
hideFilterInput
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withToasts(ChangeDatasourceModal);
|
|
@ -20,6 +20,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Alert, Badge, Col, Label, Tabs, Tab, Well } from 'react-bootstrap';
|
import { Alert, Badge, Col, Label, Tabs, Tab, Well } from 'react-bootstrap';
|
||||||
import shortid from 'shortid';
|
import shortid from 'shortid';
|
||||||
|
import styled from '@superset-ui/style';
|
||||||
import { t } from '@superset-ui/translation';
|
import { t } from '@superset-ui/translation';
|
||||||
import { SupersetClient } from '@superset-ui/connection';
|
import { SupersetClient } from '@superset-ui/connection';
|
||||||
import getClientErrorObject from '../utils/getClientErrorObject';
|
import getClientErrorObject from '../utils/getClientErrorObject';
|
||||||
|
@ -40,7 +41,21 @@ import Field from '../CRUD/Field';
|
||||||
|
|
||||||
import withToasts from '../messageToasts/enhancers/withToasts';
|
import withToasts from '../messageToasts/enhancers/withToasts';
|
||||||
|
|
||||||
import './main.less';
|
const DatasourceContainer = styled.div`
|
||||||
|
.tab-content {
|
||||||
|
height: 600px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-warning {
|
||||||
|
margin: 16px 10px 0;
|
||||||
|
color: ${({ theme }) => theme.colors.warning.base};
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-warning .bold {
|
||||||
|
font-weight: ${({ theme }) => theme.typography.weights.bold};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const checkboxGenerator = (d, onChange) => (
|
const checkboxGenerator = (d, onChange) => (
|
||||||
<CheckboxControl value={d} onChange={onChange} />
|
<CheckboxControl value={d} onChange={onChange} />
|
||||||
|
@ -220,8 +235,12 @@ export class DatasourceEditor extends React.PureComponent {
|
||||||
this.state = {
|
this.state = {
|
||||||
datasource: props.datasource,
|
datasource: props.datasource,
|
||||||
errors: [],
|
errors: [],
|
||||||
isDruid: props.datasource.type === 'druid',
|
isDruid:
|
||||||
isSqla: props.datasource.type === 'table',
|
props.datasource.type === 'druid' ||
|
||||||
|
props.datasource.datasource_type === 'druid',
|
||||||
|
isSqla:
|
||||||
|
props.datasource.datasource_type === 'table' ||
|
||||||
|
props.datasource.type === 'table',
|
||||||
databaseColumns: props.datasource.columns.filter(col => !col.expression),
|
databaseColumns: props.datasource.columns.filter(col => !col.expression),
|
||||||
calculatedColumns: props.datasource.columns.filter(
|
calculatedColumns: props.datasource.columns.filter(
|
||||||
col => !!col.expression,
|
col => !!col.expression,
|
||||||
|
@ -290,10 +309,12 @@ export class DatasourceEditor extends React.PureComponent {
|
||||||
const { datasource } = this.state;
|
const { datasource } = this.state;
|
||||||
// Handle carefully when the schema is empty
|
// Handle carefully when the schema is empty
|
||||||
const endpoint =
|
const endpoint =
|
||||||
`/datasource/external_metadata/${datasource.type}/${datasource.id}/` +
|
`/datasource/external_metadata/${
|
||||||
|
datasource.type || datasource.datasource_type
|
||||||
|
}/${datasource.id}/` +
|
||||||
`?db_id=${datasource.database.id}` +
|
`?db_id=${datasource.database.id}` +
|
||||||
`&schema=${datasource.schema || ''}` +
|
`&schema=${datasource.schema || ''}` +
|
||||||
`&table_name=${datasource.datasource_name}`;
|
`&table_name=${datasource.datasource_name || datasource.table_name}`;
|
||||||
this.setState({ metadataLoading: true });
|
this.setState({ metadataLoading: true });
|
||||||
|
|
||||||
SupersetClient.get({ endpoint })
|
SupersetClient.get({ endpoint })
|
||||||
|
@ -645,7 +666,7 @@ export class DatasourceEditor extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
const { datasource, activeTabKey } = this.state;
|
const { datasource, activeTabKey } = this.state;
|
||||||
return (
|
return (
|
||||||
<div className="Datasource">
|
<DatasourceContainer>
|
||||||
{this.renderErrors()}
|
{this.renderErrors()}
|
||||||
<Tabs
|
<Tabs
|
||||||
id="table-tabs"
|
id="table-tabs"
|
||||||
|
@ -747,7 +768,7 @@ export class DatasourceEditor extends React.PureComponent {
|
||||||
)}
|
)}
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</DatasourceContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,181 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Alert, Button, Modal } from 'react-bootstrap';
|
|
||||||
import Dialog from 'react-bootstrap-dialog';
|
|
||||||
import { t } from '@superset-ui/translation';
|
|
||||||
import { SupersetClient } from '@superset-ui/connection';
|
|
||||||
|
|
||||||
import getClientErrorObject from '../utils/getClientErrorObject';
|
|
||||||
import DatasourceEditor from '../datasource/DatasourceEditor';
|
|
||||||
import withToasts from '../messageToasts/enhancers/withToasts';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
datasource: PropTypes.object.isRequired,
|
|
||||||
show: PropTypes.bool.isRequired,
|
|
||||||
onHide: PropTypes.func,
|
|
||||||
onDatasourceSave: PropTypes.func,
|
|
||||||
addSuccessToast: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
onChange: () => {},
|
|
||||||
onHide: () => {},
|
|
||||||
onDatasourceSave: () => {},
|
|
||||||
show: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
class DatasourceModal extends React.PureComponent {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
errors: [],
|
|
||||||
datasource: props.datasource,
|
|
||||||
};
|
|
||||||
this.setSearchRef = this.setSearchRef.bind(this);
|
|
||||||
this.onDatasourceChange = this.onDatasourceChange.bind(this);
|
|
||||||
this.onClickSave = this.onClickSave.bind(this);
|
|
||||||
this.onConfirmSave = this.onConfirmSave.bind(this);
|
|
||||||
this.setDialogRef = this.setDialogRef.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickSave() {
|
|
||||||
this.dialog.show({
|
|
||||||
title: t('Confirm save'),
|
|
||||||
bsSize: 'medium',
|
|
||||||
actions: [Dialog.CancelAction(), Dialog.OKAction(this.onConfirmSave)],
|
|
||||||
body: this.renderSaveDialog(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onConfirmSave() {
|
|
||||||
SupersetClient.post({
|
|
||||||
endpoint: '/datasource/save/',
|
|
||||||
postPayload: {
|
|
||||||
data: this.state.datasource,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(({ json }) => {
|
|
||||||
this.props.addSuccessToast(t('The datasource has been saved'));
|
|
||||||
this.props.onDatasourceSave(json);
|
|
||||||
this.props.onHide();
|
|
||||||
})
|
|
||||||
.catch(response =>
|
|
||||||
getClientErrorObject(response).then(({ error, statusText }) => {
|
|
||||||
this.dialog.show({
|
|
||||||
title: 'Error',
|
|
||||||
bsSize: 'medium',
|
|
||||||
bsStyle: 'danger',
|
|
||||||
actions: [Dialog.DefaultAction('Ok', () => {}, 'btn-danger')],
|
|
||||||
body: error || statusText || t('An error has occurred'),
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDatasourceChange(datasource, errors) {
|
|
||||||
this.setState({ datasource, errors });
|
|
||||||
}
|
|
||||||
|
|
||||||
setSearchRef(searchRef) {
|
|
||||||
this.searchRef = searchRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDialogRef(ref) {
|
|
||||||
this.dialog = ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSaveDialog() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Alert bsStyle="warning" className="pointer" onClick={this.hideAlert}>
|
|
||||||
<div>
|
|
||||||
<i className="fa fa-exclamation-triangle" />{' '}
|
|
||||||
{t(`The data source configuration exposed here
|
|
||||||
affects all the charts using this datasource.
|
|
||||||
Be mindful that changing settings
|
|
||||||
here may affect other charts
|
|
||||||
in undesirable ways.`)}
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
{t('Are you sure you want to save and apply changes?')}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Modal show={this.props.show} onHide={this.props.onHide} bsSize="lg">
|
|
||||||
<Modal.Header closeButton>
|
|
||||||
<Modal.Title>
|
|
||||||
<div>
|
|
||||||
<span className="float-left">
|
|
||||||
{t('Datasource Editor for ')}
|
|
||||||
<strong>{this.props.datasource.name}</strong>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Body>
|
|
||||||
{this.props.show && (
|
|
||||||
<DatasourceEditor
|
|
||||||
datasource={this.props.datasource}
|
|
||||||
onChange={this.onDatasourceChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal.Body>
|
|
||||||
<Modal.Footer>
|
|
||||||
<span className="float-left">
|
|
||||||
<Button
|
|
||||||
bsSize="sm"
|
|
||||||
bsStyle="default"
|
|
||||||
target="_blank"
|
|
||||||
href={this.props.datasource.edit_url}
|
|
||||||
>
|
|
||||||
{t('Use Legacy Datasource Editor')}
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="float-right">
|
|
||||||
<Button
|
|
||||||
bsSize="sm"
|
|
||||||
bsStyle="primary"
|
|
||||||
className="m-r-5"
|
|
||||||
onClick={this.onClickSave}
|
|
||||||
disabled={this.state.errors.length > 0}
|
|
||||||
>
|
|
||||||
{t('Save')}
|
|
||||||
</Button>
|
|
||||||
<Button bsSize="sm" onClick={this.props.onHide}>
|
|
||||||
{t('Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Dialog ref={this.setDialogRef} />
|
|
||||||
</span>
|
|
||||||
</Modal.Footer>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DatasourceModal.propTypes = propTypes;
|
|
||||||
DatasourceModal.defaultProps = defaultProps;
|
|
||||||
|
|
||||||
export default withToasts(DatasourceModal);
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React, { FunctionComponent, useState, useRef } from 'react';
|
||||||
|
import { Alert, Button, Modal } from 'react-bootstrap';
|
||||||
|
// @ts-ignore
|
||||||
|
import Dialog from 'react-bootstrap-dialog';
|
||||||
|
import { t } from '@superset-ui/translation';
|
||||||
|
import { SupersetClient } from '@superset-ui/connection';
|
||||||
|
|
||||||
|
import getClientErrorObject from '../utils/getClientErrorObject';
|
||||||
|
import DatasourceEditor from './DatasourceEditor';
|
||||||
|
import withToasts from '../messageToasts/enhancers/withToasts';
|
||||||
|
|
||||||
|
interface DatasourceModalProps {
|
||||||
|
addSuccessToast: (msg: string) => void;
|
||||||
|
datasource: any;
|
||||||
|
onChange: () => {};
|
||||||
|
onDatasourceSave: (datasource: object, errors?: Array<any>) => {};
|
||||||
|
onHide: () => {};
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
||||||
|
addSuccessToast,
|
||||||
|
datasource,
|
||||||
|
onDatasourceSave,
|
||||||
|
onHide,
|
||||||
|
show,
|
||||||
|
}) => {
|
||||||
|
const [currentDatasource, setCurrentDatasource] = useState(datasource);
|
||||||
|
const [errors, setErrors] = useState<any[]>([]);
|
||||||
|
const dialog = useRef<any>(null);
|
||||||
|
|
||||||
|
const onConfirmSave = () => {
|
||||||
|
SupersetClient.post({
|
||||||
|
endpoint: '/datasource/save/',
|
||||||
|
postPayload: {
|
||||||
|
data: {
|
||||||
|
...currentDatasource,
|
||||||
|
type: currentDatasource.type || currentDatasource.datasource_type,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(({ json }) => {
|
||||||
|
addSuccessToast(t('The datasource has been saved'));
|
||||||
|
onDatasourceSave(json);
|
||||||
|
onHide();
|
||||||
|
})
|
||||||
|
.catch(response =>
|
||||||
|
getClientErrorObject(response).then(({ error }) => {
|
||||||
|
dialog.current.show({
|
||||||
|
title: 'Error',
|
||||||
|
bsSize: 'medium',
|
||||||
|
bsStyle: 'danger',
|
||||||
|
actions: [Dialog.DefaultAction('Ok', () => {}, 'btn-danger')],
|
||||||
|
body: error || t('An error has occurred'),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDatasourceChange = (data: object, err: Array<any>) => {
|
||||||
|
setCurrentDatasource(data);
|
||||||
|
setErrors(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSaveDialog = () => (
|
||||||
|
<div>
|
||||||
|
<Alert
|
||||||
|
bsStyle="warning"
|
||||||
|
className="pointer"
|
||||||
|
onClick={dialog.current.hideAlert}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<i className="fa fa-exclamation-triangle" />{' '}
|
||||||
|
{t(`The data source configuration exposed here
|
||||||
|
affects all the charts using this datasource.
|
||||||
|
Be mindful that changing settings
|
||||||
|
here may affect other charts
|
||||||
|
in undesirable ways.`)}
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
{t('Are you sure you want to save and apply changes?')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClickSave = () => {
|
||||||
|
dialog.current.show({
|
||||||
|
title: t('Confirm save'),
|
||||||
|
bsSize: 'medium',
|
||||||
|
actions: [Dialog.CancelAction(), Dialog.OKAction(onConfirmSave)],
|
||||||
|
body: renderSaveDialog(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal show={show} onHide={onHide} bsSize="lg">
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title>
|
||||||
|
<div>
|
||||||
|
<span className="float-left">
|
||||||
|
{t('Datasource Editor for ')}
|
||||||
|
<strong>{currentDatasource.table_name}</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
{show && (
|
||||||
|
<DatasourceEditor
|
||||||
|
datasource={currentDatasource}
|
||||||
|
onChange={onDatasourceChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<span className="float-left">
|
||||||
|
<Button
|
||||||
|
bsSize="sm"
|
||||||
|
bsStyle="default"
|
||||||
|
target="_blank"
|
||||||
|
href={currentDatasource.edit_url || currentDatasource.url}
|
||||||
|
>
|
||||||
|
{t('Use Legacy Datasource Editor')}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="float-right">
|
||||||
|
<Button
|
||||||
|
bsSize="sm"
|
||||||
|
bsStyle="primary"
|
||||||
|
className="m-r-5"
|
||||||
|
onClick={onClickSave}
|
||||||
|
disabled={errors.length > 0}
|
||||||
|
>
|
||||||
|
{t('Save')}
|
||||||
|
</Button>
|
||||||
|
<Button bsSize="sm" onClick={onHide}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Dialog ref={dialog} />
|
||||||
|
</span>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withToasts(DatasourceModal);
|
|
@ -1,33 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
@import '../../stylesheets/less/variables.less';
|
|
||||||
|
|
||||||
.Datasource .tab-content {
|
|
||||||
height: 600px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Datasource .change-warning {
|
|
||||||
margin: 16px 10px 0;
|
|
||||||
color: @warning;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Datasource .change-warning .bold {
|
|
||||||
font-weight: @font-weight-bold;
|
|
||||||
}
|
|
|
@ -27,6 +27,7 @@ import React, {
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
|
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
|
||||||
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||||
|
import DatasourceModal from 'src/datasource/DatasourceModal';
|
||||||
import DeleteModal from 'src/components/DeleteModal';
|
import DeleteModal from 'src/components/DeleteModal';
|
||||||
import ListView, { ListViewProps } from 'src/components/ListView/ListView';
|
import ListView, { ListViewProps } from 'src/components/ListView/ListView';
|
||||||
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
||||||
|
@ -146,6 +147,10 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||||
const [datasetCurrentlyDeleting, setDatasetCurrentlyDeleting] = useState<
|
const [datasetCurrentlyDeleting, setDatasetCurrentlyDeleting] = useState<
|
||||||
(Dataset & { chart_count: number; dashboard_count: number }) | null
|
(Dataset & { chart_count: number; dashboard_count: number }) | null
|
||||||
>(null);
|
>(null);
|
||||||
|
const [
|
||||||
|
datasetCurrentlyEditing,
|
||||||
|
setDatasetCurrentlyEditing,
|
||||||
|
] = useState<Dataset | null>(null);
|
||||||
const [datasets, setDatasets] = useState<any[]>([]);
|
const [datasets, setDatasets] = useState<any[]>([]);
|
||||||
const [
|
const [
|
||||||
lastFetchDataConfig,
|
lastFetchDataConfig,
|
||||||
|
@ -252,8 +257,19 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||||
|
|
||||||
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
|
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
|
||||||
|
|
||||||
const handleDatasetEdit = ({ id }: { id: number }) => {
|
const openDatasetEditModal = ({ id }: Dataset) => {
|
||||||
window.location.assign(`/tablemodelview/edit/${id}`);
|
SupersetClient.get({
|
||||||
|
endpoint: `/api/v1/dataset/${id}`,
|
||||||
|
})
|
||||||
|
.then(({ json = {} }) => {
|
||||||
|
const owners = json.result.owners.map((owner: any) => owner.id);
|
||||||
|
setDatasetCurrentlyEditing({ ...json.result, owners });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
addDangerToast(
|
||||||
|
t('An error occurred while fetching dataset related data'),
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDatasetDeleteModal = (dataset: Dataset) =>
|
const openDatasetDeleteModal = (dataset: Dataset) =>
|
||||||
|
@ -395,8 +411,8 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Cell: ({ row: { original } }: any) => {
|
Cell: ({ row: { state, original } }: any) => {
|
||||||
const handleEdit = () => handleDatasetEdit(original);
|
const handleEdit = () => openDatasetEditModal(original);
|
||||||
const handleDelete = () => openDatasetDeleteModal(original);
|
const handleDelete = () => openDatasetDeleteModal(original);
|
||||||
if (!canEdit && !canDelete) {
|
if (!canEdit && !canDelete) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -500,6 +516,8 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||||
setDatasetCurrentlyDeleting(null);
|
setDatasetCurrentlyDeleting(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeDatasetEditModal = () => setDatasetCurrentlyEditing(null);
|
||||||
|
|
||||||
const fetchData = useCallback(
|
const fetchData = useCallback(
|
||||||
({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => {
|
({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => {
|
||||||
// set loading state, cache the last config for fetching data in this component.
|
// set loading state, cache the last config for fetching data in this component.
|
||||||
|
@ -583,6 +601,12 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateDataset = () => {
|
||||||
|
if (lastFetchDataConfig) {
|
||||||
|
fetchData(lastFetchDataConfig);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SubMenu {...menuData} />
|
<SubMenu {...menuData} />
|
||||||
|
@ -627,54 +651,82 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListView
|
<>
|
||||||
className="dataset-list-view"
|
{datasetCurrentlyDeleting && (
|
||||||
columns={columns}
|
<DeleteModal
|
||||||
data={datasets}
|
description={t(
|
||||||
count={datasetCount}
|
`The dataset ${datasetCurrentlyDeleting.table_name} is linked to
|
||||||
pageSize={PAGE_SIZE}
|
${datasetCurrentlyDeleting.chart_count} charts that appear on
|
||||||
fetchData={fetchData}
|
${datasetCurrentlyDeleting.dashboard_count} dashboards.
|
||||||
filters={filterTypes}
|
Are you sure you want to continue? Deleting the dataset will break
|
||||||
loading={loading}
|
those objects.`,
|
||||||
initialSort={initialSort}
|
)}
|
||||||
bulkActions={bulkActions}
|
onConfirm={() =>
|
||||||
bulkSelectEnabled={bulkSelectEnabled}
|
handleDatasetDelete(datasetCurrentlyDeleting)
|
||||||
disableBulkSelect={() => setBulkSelectEnabled(false)}
|
}
|
||||||
renderBulkSelectCopy={selected => {
|
onHide={closeDatasetDeleteModal}
|
||||||
const { virtualCount, physicalCount } = selected.reduce(
|
open
|
||||||
(acc, e) => {
|
title={t('Delete Dataset?')}
|
||||||
if (e.original.kind === 'physical') acc.physicalCount += 1;
|
/>
|
||||||
else if (e.original.kind === 'virtual')
|
)}
|
||||||
acc.virtualCount += 1;
|
{datasetCurrentlyEditing && (
|
||||||
return acc;
|
<DatasourceModal
|
||||||
},
|
datasource={datasetCurrentlyEditing}
|
||||||
{ virtualCount: 0, physicalCount: 0 },
|
onDatasourceSave={handleUpdateDataset}
|
||||||
);
|
onHide={closeDatasetEditModal}
|
||||||
|
show
|
||||||
if (!selected.length) {
|
/>
|
||||||
return t('0 Selected');
|
)}
|
||||||
} else if (virtualCount && !physicalCount) {
|
<ListView
|
||||||
return t(
|
className="dataset-list-view"
|
||||||
'%s Selected (Virtual)',
|
columns={columns}
|
||||||
selected.length,
|
data={datasets}
|
||||||
virtualCount,
|
count={datasetCount}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
fetchData={fetchData}
|
||||||
|
filters={filterTypes}
|
||||||
|
loading={loading}
|
||||||
|
initialSort={initialSort}
|
||||||
|
bulkActions={bulkActions}
|
||||||
|
bulkSelectEnabled={bulkSelectEnabled}
|
||||||
|
disableBulkSelect={() => setBulkSelectEnabled(false)}
|
||||||
|
renderBulkSelectCopy={selected => {
|
||||||
|
const { virtualCount, physicalCount } = selected.reduce(
|
||||||
|
(acc, e) => {
|
||||||
|
if (e.original.kind === 'physical')
|
||||||
|
acc.physicalCount += 1;
|
||||||
|
else if (e.original.kind === 'virtual')
|
||||||
|
acc.virtualCount += 1;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ virtualCount: 0, physicalCount: 0 },
|
||||||
);
|
);
|
||||||
} else if (physicalCount && !virtualCount) {
|
|
||||||
|
if (!selected.length) {
|
||||||
|
return t('0 Selected');
|
||||||
|
} else if (virtualCount && !physicalCount) {
|
||||||
|
return t(
|
||||||
|
'%s Selected (Virtual)',
|
||||||
|
selected.length,
|
||||||
|
virtualCount,
|
||||||
|
);
|
||||||
|
} else if (physicalCount && !virtualCount) {
|
||||||
|
return t(
|
||||||
|
'%s Selected (Physical)',
|
||||||
|
selected.length,
|
||||||
|
physicalCount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return t(
|
return t(
|
||||||
'%s Selected (Physical)',
|
'%s Selected (%s Physical, %s Virtual)',
|
||||||
selected.length,
|
selected.length,
|
||||||
physicalCount,
|
physicalCount,
|
||||||
|
virtualCount,
|
||||||
);
|
);
|
||||||
}
|
}}
|
||||||
|
/>
|
||||||
return t(
|
</>
|
||||||
'%s Selected (%s Physical, %s Virtual)',
|
|
||||||
selected.length,
|
|
||||||
physicalCount,
|
|
||||||
virtualCount,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</ConfirmStatusChange>
|
</ConfirmStatusChange>
|
||||||
|
|
|
@ -540,6 +540,10 @@ class DruidDatasource(Model, BaseDatasource):
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return self.datasource_name
|
return self.datasource_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def datasource_type(self) -> str:
|
||||||
|
return self.type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def schema(self) -> Optional[str]:
|
def schema(self) -> Optional[str]:
|
||||||
ds_name = self.datasource_name or ""
|
ds_name = self.datasource_name or ""
|
||||||
|
|
|
@ -493,6 +493,14 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
|
||||||
def datasource_name(self) -> str:
|
def datasource_name(self) -> str:
|
||||||
return self.table_name
|
return self.table_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def datasource_type(self) -> str:
|
||||||
|
return self.type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def database_name(self) -> str:
|
||||||
|
return self.database.name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_datasource_by_name(
|
def get_datasource_by_name(
|
||||||
cls,
|
cls,
|
||||||
|
|
|
@ -100,6 +100,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
||||||
"database.database_name",
|
"database.database_name",
|
||||||
]
|
]
|
||||||
show_columns = [
|
show_columns = [
|
||||||
|
"id",
|
||||||
"database.database_name",
|
"database.database_name",
|
||||||
"database.id",
|
"database.id",
|
||||||
"table_name",
|
"table_name",
|
||||||
|
@ -120,6 +121,8 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
||||||
"owners.last_name",
|
"owners.last_name",
|
||||||
"columns",
|
"columns",
|
||||||
"metrics",
|
"metrics",
|
||||||
|
"datasource_type",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
add_model_schema = DatasetPostSchema()
|
add_model_schema = DatasetPostSchema()
|
||||||
edit_model_schema = DatasetPutSchema()
|
edit_model_schema = DatasetPutSchema()
|
||||||
|
|
Loading…
Reference in New Issue