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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { Modal } from 'react-bootstrap';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { shallow } from 'enzyme';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import thunk from 'redux-thunk';
|
||||
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 waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||
import mockDatasource from '../../fixtures/mockDatasource';
|
||||
|
||||
const props = {
|
||||
const mockStore = configureStore([thunk]);
|
||||
const store = mockStore({});
|
||||
|
||||
const mockedProps = {
|
||||
addDangerToast: () => {},
|
||||
onDatasourceSave: sinon.spy(),
|
||||
onChange: () => {},
|
||||
|
@ -37,62 +43,53 @@ const props = {
|
|||
|
||||
const datasource = mockDatasource['7__table'];
|
||||
const datasourceData = {
|
||||
id: datasource.name,
|
||||
id: datasource.id,
|
||||
type: datasource.type,
|
||||
uid: datasource.id,
|
||||
};
|
||||
|
||||
const DATASOURCES_ENDPOINT = 'glob:*/superset/datasources/';
|
||||
const DATASOURCE_ENDPOINT = `glob:*/datasource/get/${datasourceData.type}/${datasourceData.id}`;
|
||||
const DATASOURCES_PAYLOAD = { json: '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', () => {
|
||||
const mockStore = configureStore([thunk]);
|
||||
const store = mockStore({});
|
||||
fetchMock.get(DATASOURCES_ENDPOINT, DATASOURCES_PAYLOAD);
|
||||
|
||||
let wrapper;
|
||||
let el;
|
||||
let inst;
|
||||
|
||||
beforeEach(() => {
|
||||
el = <ChangeDatasourceModal {...props} />;
|
||||
wrapper = shallow(el, { context: { store } }).dive();
|
||||
inst = wrapper.instance();
|
||||
beforeEach(async () => {
|
||||
wrapper = await mountAndWait();
|
||||
});
|
||||
|
||||
it('is valid', () => {
|
||||
expect(React.isValidElement(el)).toBe(true);
|
||||
it('renders', () => {
|
||||
expect(wrapper.find(ChangeDatasourceModal)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders a Modal', () => {
|
||||
expect(wrapper.find(Modal)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('fetches datasources', () => {
|
||||
return new Promise(done => {
|
||||
inst.onEnterModal();
|
||||
setTimeout(() => {
|
||||
expect(fetchMock.calls(DATASOURCES_ENDPOINT)).toHaveLength(1);
|
||||
fetchMock.reset();
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
it('fetches datasources', async () => {
|
||||
expect(fetchMock.calls(/superset\/datasources/)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('changes the datasource', () => {
|
||||
return new Promise(done => {
|
||||
fetchMock.get(DATASOURCE_ENDPOINT, DATASOURCE_PAYLOAD);
|
||||
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);
|
||||
it('changes the datasource', async () => {
|
||||
act(() => {
|
||||
wrapper.find('.datasource-link').at(0).props().onClick(datasourceData);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(fetchMock.calls(/datasource\/get\/table\/7/)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,66 +17,80 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Modal } from 'react-bootstrap';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { shallow } from 'enzyme';
|
||||
import { mount } from 'enzyme';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import thunk from 'redux-thunk';
|
||||
import sinon from 'sinon';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
|
||||
|
||||
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||
import DatasourceModal from 'src/datasource/DatasourceModal';
|
||||
import DatasourceEditor from 'src/datasource/DatasourceEditor';
|
||||
import mockDatasource from '../../fixtures/mockDatasource';
|
||||
|
||||
const props = {
|
||||
datasource: mockDatasource['7__table'],
|
||||
const mockStore = configureStore([thunk]);
|
||||
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: () => {},
|
||||
addDangerToast: () => {},
|
||||
onChange: () => {},
|
||||
show: true,
|
||||
onHide: () => {},
|
||||
show: true,
|
||||
onDatasourceSave: sinon.spy(),
|
||||
};
|
||||
|
||||
const SAVE_ENDPOINT = 'glob:*/datasource/save/';
|
||||
const SAVE_PAYLOAD = { new: 'data' };
|
||||
async function mountAndWait(props = mockedProps) {
|
||||
const mounted = mount(<DatasourceModal {...props} />, {
|
||||
context: { store },
|
||||
wrappingComponent: ThemeProvider,
|
||||
wrappingComponentProps: { theme: supersetTheme },
|
||||
});
|
||||
await waitForComponentToPaint(mounted);
|
||||
|
||||
return mounted;
|
||||
}
|
||||
|
||||
describe('DatasourceModal', () => {
|
||||
const mockStore = configureStore([thunk]);
|
||||
const store = mockStore({});
|
||||
fetchMock.post(SAVE_ENDPOINT, SAVE_PAYLOAD);
|
||||
const callsP = fetchMock.put(SAVE_ENDPOINT, SAVE_PAYLOAD);
|
||||
|
||||
let wrapper;
|
||||
let el;
|
||||
let inst;
|
||||
|
||||
beforeEach(() => {
|
||||
el = <DatasourceModal {...props} />;
|
||||
wrapper = shallow(el, { context: { store } }).dive();
|
||||
inst = wrapper.instance();
|
||||
beforeEach(async () => {
|
||||
wrapper = await mountAndWait();
|
||||
});
|
||||
|
||||
it('is valid', () => {
|
||||
expect(React.isValidElement(el)).toBe(true);
|
||||
it('renders', () => {
|
||||
expect(wrapper.find(DatasourceModal)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders a Modal', () => {
|
||||
expect(wrapper.find(Modal)).toHaveLength(1);
|
||||
expect(wrapper.find(Modal)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('renders a DatasourceEditor', () => {
|
||||
expect(wrapper.find(DatasourceEditor)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('saves on confirm', () => {
|
||||
return new Promise(done => {
|
||||
inst.onConfirmSave();
|
||||
setTimeout(() => {
|
||||
expect(fetchMock.calls(SAVE_ENDPOINT)).toHaveLength(1);
|
||||
expect(props.onDatasourceSave.getCall(0).args[0]).toEqual(SAVE_PAYLOAD);
|
||||
fetchMock.reset();
|
||||
done();
|
||||
}, 0);
|
||||
it('saves on confirm', async () => {
|
||||
act(() => {
|
||||
wrapper.find('[className="m-r-5"]').props().onClick();
|
||||
});
|
||||
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 { Alert, Badge, Col, Label, Tabs, Tab, Well } from 'react-bootstrap';
|
||||
import shortid from 'shortid';
|
||||
import styled from '@superset-ui/style';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { SupersetClient } from '@superset-ui/connection';
|
||||
import getClientErrorObject from '../utils/getClientErrorObject';
|
||||
|
@ -40,7 +41,21 @@ import Field from '../CRUD/Field';
|
|||
|
||||
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) => (
|
||||
<CheckboxControl value={d} onChange={onChange} />
|
||||
|
@ -220,8 +235,12 @@ export class DatasourceEditor extends React.PureComponent {
|
|||
this.state = {
|
||||
datasource: props.datasource,
|
||||
errors: [],
|
||||
isDruid: props.datasource.type === 'druid',
|
||||
isSqla: props.datasource.type === 'table',
|
||||
isDruid:
|
||||
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),
|
||||
calculatedColumns: props.datasource.columns.filter(
|
||||
col => !!col.expression,
|
||||
|
@ -290,10 +309,12 @@ export class DatasourceEditor extends React.PureComponent {
|
|||
const { datasource } = this.state;
|
||||
// Handle carefully when the schema is empty
|
||||
const endpoint =
|
||||
`/datasource/external_metadata/${datasource.type}/${datasource.id}/` +
|
||||
`/datasource/external_metadata/${
|
||||
datasource.type || datasource.datasource_type
|
||||
}/${datasource.id}/` +
|
||||
`?db_id=${datasource.database.id}` +
|
||||
`&schema=${datasource.schema || ''}` +
|
||||
`&table_name=${datasource.datasource_name}`;
|
||||
`&table_name=${datasource.datasource_name || datasource.table_name}`;
|
||||
this.setState({ metadataLoading: true });
|
||||
|
||||
SupersetClient.get({ endpoint })
|
||||
|
@ -645,7 +666,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||
render() {
|
||||
const { datasource, activeTabKey } = this.state;
|
||||
return (
|
||||
<div className="Datasource">
|
||||
<DatasourceContainer>
|
||||
{this.renderErrors()}
|
||||
<Tabs
|
||||
id="table-tabs"
|
||||
|
@ -747,7 +768,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||
)}
|
||||
</Tab>
|
||||
</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 { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
|
||||
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||
import DatasourceModal from 'src/datasource/DatasourceModal';
|
||||
import DeleteModal from 'src/components/DeleteModal';
|
||||
import ListView, { ListViewProps } from 'src/components/ListView/ListView';
|
||||
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
||||
|
@ -146,6 +147,10 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||
const [datasetCurrentlyDeleting, setDatasetCurrentlyDeleting] = useState<
|
||||
(Dataset & { chart_count: number; dashboard_count: number }) | null
|
||||
>(null);
|
||||
const [
|
||||
datasetCurrentlyEditing,
|
||||
setDatasetCurrentlyEditing,
|
||||
] = useState<Dataset | null>(null);
|
||||
const [datasets, setDatasets] = useState<any[]>([]);
|
||||
const [
|
||||
lastFetchDataConfig,
|
||||
|
@ -252,8 +257,19 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||
|
||||
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
|
||||
|
||||
const handleDatasetEdit = ({ id }: { id: number }) => {
|
||||
window.location.assign(`/tablemodelview/edit/${id}`);
|
||||
const openDatasetEditModal = ({ id }: Dataset) => {
|
||||
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) =>
|
||||
|
@ -395,8 +411,8 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ row: { original } }: any) => {
|
||||
const handleEdit = () => handleDatasetEdit(original);
|
||||
Cell: ({ row: { state, original } }: any) => {
|
||||
const handleEdit = () => openDatasetEditModal(original);
|
||||
const handleDelete = () => openDatasetDeleteModal(original);
|
||||
if (!canEdit && !canDelete) {
|
||||
return null;
|
||||
|
@ -500,6 +516,8 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||
setDatasetCurrentlyDeleting(null);
|
||||
};
|
||||
|
||||
const closeDatasetEditModal = () => setDatasetCurrentlyEditing(null);
|
||||
|
||||
const fetchData = useCallback(
|
||||
({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => {
|
||||
// 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 (
|
||||
<>
|
||||
<SubMenu {...menuData} />
|
||||
|
@ -627,54 +651,82 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||
: [];
|
||||
|
||||
return (
|
||||
<ListView
|
||||
className="dataset-list-view"
|
||||
columns={columns}
|
||||
data={datasets}
|
||||
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 },
|
||||
);
|
||||
|
||||
if (!selected.length) {
|
||||
return t('0 Selected');
|
||||
} else if (virtualCount && !physicalCount) {
|
||||
return t(
|
||||
'%s Selected (Virtual)',
|
||||
selected.length,
|
||||
virtualCount,
|
||||
<>
|
||||
{datasetCurrentlyDeleting && (
|
||||
<DeleteModal
|
||||
description={t(
|
||||
`The dataset ${datasetCurrentlyDeleting.table_name} is linked to
|
||||
${datasetCurrentlyDeleting.chart_count} charts that appear on
|
||||
${datasetCurrentlyDeleting.dashboard_count} dashboards.
|
||||
Are you sure you want to continue? Deleting the dataset will break
|
||||
those objects.`,
|
||||
)}
|
||||
onConfirm={() =>
|
||||
handleDatasetDelete(datasetCurrentlyDeleting)
|
||||
}
|
||||
onHide={closeDatasetDeleteModal}
|
||||
open
|
||||
title={t('Delete Dataset?')}
|
||||
/>
|
||||
)}
|
||||
{datasetCurrentlyEditing && (
|
||||
<DatasourceModal
|
||||
datasource={datasetCurrentlyEditing}
|
||||
onDatasourceSave={handleUpdateDataset}
|
||||
onHide={closeDatasetEditModal}
|
||||
show
|
||||
/>
|
||||
)}
|
||||
<ListView
|
||||
className="dataset-list-view"
|
||||
columns={columns}
|
||||
data={datasets}
|
||||
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(
|
||||
'%s Selected (Physical)',
|
||||
'%s Selected (%s Physical, %s Virtual)',
|
||||
selected.length,
|
||||
physicalCount,
|
||||
virtualCount,
|
||||
);
|
||||
}
|
||||
|
||||
return t(
|
||||
'%s Selected (%s Physical, %s Virtual)',
|
||||
selected.length,
|
||||
physicalCount,
|
||||
virtualCount,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</ConfirmStatusChange>
|
||||
|
|
|
@ -540,6 +540,10 @@ class DruidDatasource(Model, BaseDatasource):
|
|||
def name(self) -> str:
|
||||
return self.datasource_name
|
||||
|
||||
@property
|
||||
def datasource_type(self) -> str:
|
||||
return self.type
|
||||
|
||||
@property
|
||||
def schema(self) -> Optional[str]:
|
||||
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:
|
||||
return self.table_name
|
||||
|
||||
@property
|
||||
def datasource_type(self) -> str:
|
||||
return self.type
|
||||
|
||||
@property
|
||||
def database_name(self) -> str:
|
||||
return self.database.name
|
||||
|
||||
@classmethod
|
||||
def get_datasource_by_name(
|
||||
cls,
|
||||
|
|
|
@ -100,6 +100,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
|||
"database.database_name",
|
||||
]
|
||||
show_columns = [
|
||||
"id",
|
||||
"database.database_name",
|
||||
"database.id",
|
||||
"table_name",
|
||||
|
@ -120,6 +121,8 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
|||
"owners.last_name",
|
||||
"columns",
|
||||
"metrics",
|
||||
"datasource_type",
|
||||
"url",
|
||||
]
|
||||
add_model_schema = DatasetPostSchema()
|
||||
edit_model_schema = DatasetPutSchema()
|
||||
|
|
Loading…
Reference in New Issue