[table editor] allow selecting physical table (#6046)
* [table editor] allow selecting physical table * Using classes for padding
This commit is contained in:
parent
4667f0c288
commit
da813b7ee3
|
|
@ -0,0 +1,201 @@
|
|||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { shallow } from 'enzyme';
|
||||
import sinon from 'sinon';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { table, defaultQueryEditor, initialState, tables } from '../sqllab/fixtures';
|
||||
import TableSelector from '../../../src/components/TableSelector';
|
||||
|
||||
describe('TableSelector', () => {
|
||||
let mockedProps;
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
const store = mockStore(initialState);
|
||||
let wrapper;
|
||||
let inst;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedProps = {
|
||||
dbId: 1,
|
||||
schema: 'main',
|
||||
onSchemaChange: sinon.stub(),
|
||||
onDbChange: sinon.stub(),
|
||||
getDbList: sinon.stub(),
|
||||
onTableChange: sinon.stub(),
|
||||
onChange: sinon.stub(),
|
||||
tableNameSticky: true,
|
||||
tableName: '',
|
||||
database: { id: 1, database_name: 'main' },
|
||||
horizontal: false,
|
||||
sqlLabMode: true,
|
||||
clearable: false,
|
||||
handleError: sinon.stub(),
|
||||
};
|
||||
wrapper = shallow(<TableSelector {...mockedProps} />, {
|
||||
context: { store },
|
||||
});
|
||||
inst = wrapper.instance();
|
||||
});
|
||||
|
||||
it('is valid', () => {
|
||||
expect(React.isValidElement(<TableSelector {...mockedProps} />)).toBe(true);
|
||||
});
|
||||
|
||||
describe('onDatabaseChange', () => {
|
||||
it('should fetch schemas', () => {
|
||||
sinon.stub(inst, 'fetchSchemas');
|
||||
inst.onDatabaseChange({ id: 1 });
|
||||
expect(inst.fetchSchemas.getCall(0).args[0]).toBe(1);
|
||||
inst.fetchSchemas.restore();
|
||||
});
|
||||
it('should clear tableOptions', () => {
|
||||
inst.onDatabaseChange();
|
||||
expect(wrapper.state().tableOptions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTableNamesBySubStr', () => {
|
||||
const GET_TABLE_NAMES_GLOB = 'glob:*/superset/tables/1/main/*';
|
||||
|
||||
afterEach(fetchMock.resetHistory);
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
it('should handle empty', () =>
|
||||
inst
|
||||
.getTableNamesBySubStr('')
|
||||
.then((data) => {
|
||||
expect(data).toEqual({ options: [] });
|
||||
}));
|
||||
|
||||
it('should handle table name', () => {
|
||||
const queryEditor = {
|
||||
...defaultQueryEditor,
|
||||
dbId: 1,
|
||||
schema: 'main',
|
||||
};
|
||||
|
||||
const mockTableOptions = { options: [table] };
|
||||
wrapper.setProps({ queryEditor });
|
||||
fetchMock.get(GET_TABLE_NAMES_GLOB, mockTableOptions, { overwriteRoutes: true });
|
||||
|
||||
wrapper
|
||||
.instance()
|
||||
.getTableNamesBySubStr('my table')
|
||||
.then((data) => {
|
||||
expect(fetchMock.calls(GET_TABLE_NAMES_GLOB)).toHaveLength(1);
|
||||
expect(data).toEqual(mockTableOptions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchTables', () => {
|
||||
const FETCH_TABLES_GLOB = 'glob:*/superset/tables/1/main/*/*/';
|
||||
afterEach(fetchMock.resetHistory);
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
it('should clear table options', () => {
|
||||
inst.fetchTables(true);
|
||||
expect(wrapper.state().tableOptions).toEqual([]);
|
||||
expect(wrapper.state().filterOptions).toBeNull();
|
||||
});
|
||||
|
||||
it('should fetch table options', () => {
|
||||
fetchMock.get(FETCH_TABLES_GLOB, tables, { overwriteRoutes: true });
|
||||
inst
|
||||
.fetchTables(true, 'birth_names')
|
||||
.then(() => {
|
||||
expect(wrapper.state().tableOptions).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch a danger toast on error', () => {
|
||||
fetchMock.get(FETCH_TABLES_GLOB, { throws: 'error' }, { overwriteRoutes: true });
|
||||
|
||||
wrapper
|
||||
.instance()
|
||||
.fetchTables(true, 'birth_names')
|
||||
.then(() => {
|
||||
expect(wrapper.state().tableOptions).toEqual([]);
|
||||
expect(wrapper.state().tableOptions).toHaveLength(0);
|
||||
expect(mockedProps.handleError.callCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSchemas', () => {
|
||||
const FETCH_SCHEMAS_GLOB = 'glob:*/superset/schemas/*/*/';
|
||||
afterEach(fetchMock.resetHistory);
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
it('should fetch schema options', () => {
|
||||
const schemaOptions = {
|
||||
schemas: ['main', 'erf', 'superset'],
|
||||
};
|
||||
fetchMock.get(FETCH_SCHEMAS_GLOB, schemaOptions, { overwriteRoutes: true });
|
||||
|
||||
wrapper
|
||||
.instance()
|
||||
.fetchSchemas(1)
|
||||
.then(() => {
|
||||
expect(fetchMock.calls(FETCH_SCHEMAS_GLOB)).toHaveLength(1);
|
||||
expect(wrapper.state().schemaOptions).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch a danger toast on error', () => {
|
||||
const handleErrors = sinon.stub();
|
||||
expect(handleErrors.callCount).toBe(0);
|
||||
wrapper.setProps({ handleErrors });
|
||||
fetchMock.get(FETCH_SCHEMAS_GLOB, { throws: new Error('Bad kitty') }, { overwriteRoutes: true });
|
||||
wrapper
|
||||
.instance()
|
||||
.fetchSchemas(123)
|
||||
.then(() => {
|
||||
expect(wrapper.state().schemaOptions).toEqual([]);
|
||||
expect(handleErrors.callCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeTable', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(wrapper.instance(), 'fetchTables');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.instance().fetchTables.restore();
|
||||
});
|
||||
|
||||
it('test 1', () => {
|
||||
wrapper.instance().changeTable({
|
||||
value: 'birth_names',
|
||||
label: 'birth_names',
|
||||
});
|
||||
expect(wrapper.state().tableName).toBe('birth_names');
|
||||
});
|
||||
|
||||
it('test 2', () => {
|
||||
wrapper.instance().changeTable({
|
||||
value: 'main.my_table',
|
||||
label: 'my_table',
|
||||
});
|
||||
expect(mockedProps.onTableChange.getCall(0).args[0]).toBe('my_table');
|
||||
expect(mockedProps.onTableChange.getCall(0).args[1]).toBe('main');
|
||||
});
|
||||
});
|
||||
|
||||
it('changeSchema', () => {
|
||||
sinon.stub(wrapper.instance(), 'fetchTables');
|
||||
|
||||
wrapper.instance().changeSchema({ label: 'main', value: 'main' });
|
||||
expect(wrapper.instance().fetchTables.callCount).toBe(1);
|
||||
expect(mockedProps.onChange.callCount).toBe(1);
|
||||
wrapper.instance().changeSchema();
|
||||
expect(wrapper.instance().fetchTables.callCount).toBe(2);
|
||||
expect(mockedProps.onChange.callCount).toBe(2);
|
||||
|
||||
wrapper.instance().fetchTables.restore();
|
||||
});
|
||||
});
|
||||
|
|
@ -2,10 +2,9 @@ import React from 'react';
|
|||
import configureStore from 'redux-mock-store';
|
||||
import { shallow } from 'enzyme';
|
||||
import sinon from 'sinon';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { table, defaultQueryEditor, databases, initialState, tables } from './fixtures';
|
||||
import { table, defaultQueryEditor, initialState } from './fixtures';
|
||||
import SqlEditorLeftBar from '../../../src/SqlLab/components/SqlEditorLeftBar';
|
||||
import TableElement from '../../../src/SqlLab/components/TableElement';
|
||||
|
||||
|
|
@ -32,7 +31,7 @@ describe('SqlEditorLeftBar', () => {
|
|||
beforeEach(() => {
|
||||
wrapper = shallow(<SqlEditorLeftBar {...mockedProps} />, {
|
||||
context: { store },
|
||||
}).dive();
|
||||
});
|
||||
});
|
||||
|
||||
it('is valid', () => {
|
||||
|
|
@ -43,189 +42,4 @@ describe('SqlEditorLeftBar', () => {
|
|||
expect(wrapper.find(TableElement)).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe('onDatabaseChange', () => {
|
||||
it('should fetch schemas', () => {
|
||||
sinon.stub(wrapper.instance(), 'fetchSchemas');
|
||||
wrapper.instance().onDatabaseChange({ value: 1, label: 'main' });
|
||||
expect(wrapper.instance().fetchSchemas.getCall(0).args[0]).toBe(1);
|
||||
wrapper.instance().fetchSchemas.restore();
|
||||
});
|
||||
it('should clear tableOptions', () => {
|
||||
wrapper.instance().onDatabaseChange();
|
||||
expect(wrapper.state().tableOptions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTableNamesBySubStr', () => {
|
||||
const GET_TABLE_NAMES_GLOB = 'glob:*/superset/tables/1/main/*';
|
||||
|
||||
afterEach(fetchMock.resetHistory);
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
it('should handle empty', () =>
|
||||
wrapper
|
||||
.instance()
|
||||
.getTableNamesBySubStr('')
|
||||
.then((data) => {
|
||||
expect(data).toEqual({ options: [] });
|
||||
}));
|
||||
|
||||
it('should handle table name', () => {
|
||||
const queryEditor = {
|
||||
...defaultQueryEditor,
|
||||
dbId: 1,
|
||||
schema: 'main',
|
||||
};
|
||||
|
||||
const mockTableOptions = { options: [table] };
|
||||
wrapper.setProps({ queryEditor });
|
||||
fetchMock.get(GET_TABLE_NAMES_GLOB, mockTableOptions, { overwriteRoutes: true });
|
||||
|
||||
return wrapper
|
||||
.instance()
|
||||
.getTableNamesBySubStr('my table')
|
||||
.then((data) => {
|
||||
expect(fetchMock.calls(GET_TABLE_NAMES_GLOB)).toHaveLength(1);
|
||||
expect(data).toEqual(mockTableOptions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('dbMutator should build databases options', () => {
|
||||
const options = wrapper.instance().dbMutator(databases);
|
||||
expect(options).toEqual([
|
||||
{ value: 1, label: 'main' },
|
||||
{ value: 208, label: 'Presto - Gold' },
|
||||
]);
|
||||
});
|
||||
|
||||
describe('fetchTables', () => {
|
||||
const FETCH_TABLES_GLOB = 'glob:*/superset/tables/1/main/birth_names/true/';
|
||||
afterEach(fetchMock.resetHistory);
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
it('should clear table options', () => {
|
||||
wrapper.instance().fetchTables(1);
|
||||
expect(wrapper.state().tableOptions).toEqual([]);
|
||||
expect(wrapper.state().filterOptions).toBeNull();
|
||||
});
|
||||
|
||||
it('should fetch table options', () => {
|
||||
expect.assertions(2);
|
||||
fetchMock.get(FETCH_TABLES_GLOB, tables, { overwriteRoutes: true });
|
||||
|
||||
return wrapper
|
||||
.instance()
|
||||
.fetchTables(1, 'main', true, 'birth_names')
|
||||
.then(() => {
|
||||
expect(fetchMock.calls(FETCH_TABLES_GLOB)).toHaveLength(1);
|
||||
expect(wrapper.state().tableLength).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch a danger toast on error', () => {
|
||||
const dangerToastSpy = sinon.spy();
|
||||
|
||||
wrapper.setProps({
|
||||
actions: {
|
||||
addDangerToast: dangerToastSpy,
|
||||
},
|
||||
});
|
||||
|
||||
expect.assertions(4);
|
||||
fetchMock.get(FETCH_TABLES_GLOB, { throws: 'error' }, { overwriteRoutes: true });
|
||||
|
||||
return wrapper
|
||||
.instance()
|
||||
.fetchTables(1, 'main', true, 'birth_names')
|
||||
.then(() => {
|
||||
expect(fetchMock.calls(FETCH_TABLES_GLOB)).toHaveLength(1);
|
||||
expect(wrapper.state().tableOptions).toEqual([]);
|
||||
expect(wrapper.state().tableLength).toBe(0);
|
||||
expect(dangerToastSpy.callCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSchemas', () => {
|
||||
const FETCH_SCHEMAS_GLOB = 'glob:*/superset/schemas/*';
|
||||
afterEach(fetchMock.resetHistory);
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
it('should fetch schema options', () => {
|
||||
expect.assertions(2);
|
||||
const schemaOptions = {
|
||||
schemas: ['main', 'erf', 'superset'],
|
||||
};
|
||||
fetchMock.get(FETCH_SCHEMAS_GLOB, schemaOptions, { overwriteRoutes: true });
|
||||
|
||||
return wrapper
|
||||
.instance()
|
||||
.fetchSchemas(1)
|
||||
.then(() => {
|
||||
expect(fetchMock.calls(FETCH_SCHEMAS_GLOB)).toHaveLength(1);
|
||||
expect(wrapper.state().schemaOptions).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch a danger toast on error', () => {
|
||||
const dangerToastSpy = sinon.spy();
|
||||
|
||||
wrapper.setProps({
|
||||
actions: {
|
||||
addDangerToast: dangerToastSpy,
|
||||
},
|
||||
});
|
||||
|
||||
expect.assertions(3);
|
||||
|
||||
fetchMock.get(FETCH_SCHEMAS_GLOB, { throws: 'error' }, { overwriteRoutes: true });
|
||||
|
||||
return wrapper
|
||||
.instance()
|
||||
.fetchSchemas(123)
|
||||
.then(() => {
|
||||
expect(fetchMock.calls(FETCH_SCHEMAS_GLOB)).toHaveLength(1);
|
||||
expect(wrapper.state().schemaOptions).toEqual([]);
|
||||
expect(dangerToastSpy.callCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeTable', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(wrapper.instance(), 'fetchTables');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.instance().fetchTables.restore();
|
||||
});
|
||||
|
||||
it('test 1', () => {
|
||||
wrapper.instance().changeTable({
|
||||
value: 'birth_names',
|
||||
label: 'birth_names',
|
||||
});
|
||||
expect(wrapper.state().tableName).toBe('birth_names');
|
||||
});
|
||||
|
||||
it('test 2', () => {
|
||||
wrapper.instance().changeTable({
|
||||
value: 'main.my_table',
|
||||
label: 'my_table',
|
||||
});
|
||||
expect(wrapper.instance().fetchTables.getCall(0).args[1]).toBe('main');
|
||||
});
|
||||
});
|
||||
|
||||
it('changeSchema', () => {
|
||||
sinon.stub(wrapper.instance(), 'fetchTables');
|
||||
|
||||
wrapper.instance().changeSchema({ label: 'main', value: 'main' });
|
||||
expect(wrapper.instance().fetchTables.getCall(0).args[1]).toBe('main');
|
||||
wrapper.instance().changeSchema();
|
||||
expect(wrapper.instance().fetchTables.getCall(1).args[1]).toBeNull();
|
||||
|
||||
wrapper.instance().fetchTables.restore();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -309,7 +309,6 @@ export const databases = {
|
|||
],
|
||||
};
|
||||
export const tables = {
|
||||
tableLength: 3,
|
||||
options: [
|
||||
{
|
||||
value: 'birth_names',
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ControlLabel, Button } from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import Select from 'react-virtualized-select';
|
||||
import createFilterOptions from 'react-select-fast-filter-options';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { SupersetClient } from '@superset-ui/connection';
|
||||
|
||||
import TableElement from './TableElement';
|
||||
import AsyncSelect from '../../components/AsyncSelect';
|
||||
import RefreshLabel from '../../components/RefreshLabel';
|
||||
import TableSelector from '../../components/TableSelector';
|
||||
|
||||
const propTypes = {
|
||||
queryEditor: PropTypes.object.isRequired,
|
||||
|
|
@ -26,7 +21,7 @@ const defaultProps = {
|
|||
offline: false,
|
||||
};
|
||||
|
||||
class SqlEditorLeftBar extends React.PureComponent {
|
||||
export default class SqlEditorLeftBar extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
|
@ -35,33 +30,23 @@ class SqlEditorLeftBar extends React.PureComponent {
|
|||
tableLoading: false,
|
||||
tableOptions: [],
|
||||
};
|
||||
this.resetState = this.resetState.bind(this);
|
||||
this.onSchemaChange = this.onSchemaChange.bind(this);
|
||||
this.onDbChange = this.onDbChange.bind(this);
|
||||
this.getDbList = this.getDbList.bind(this);
|
||||
this.onTableChange = this.onTableChange.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.fetchSchemas(this.props.queryEditor.dbId);
|
||||
this.fetchTables(this.props.queryEditor.dbId, this.props.queryEditor.schema);
|
||||
onSchemaChange(schema) {
|
||||
this.props.actions.queryEditorSetSchema(this.props.queryEditor, schema);
|
||||
}
|
||||
|
||||
onDatabaseChange(db, force) {
|
||||
const val = db ? db.value : null;
|
||||
this.setState(() => ({ schemaOptions: [], tableOptions: [] }));
|
||||
this.props.actions.queryEditorSetSchema(this.props.queryEditor, null);
|
||||
this.props.actions.queryEditorSetDb(this.props.queryEditor, val);
|
||||
if (db) {
|
||||
this.fetchSchemas(val, force || false);
|
||||
}
|
||||
onDbChange(db) {
|
||||
this.props.actions.queryEditorSetDb(this.props.queryEditor, db.id);
|
||||
}
|
||||
|
||||
getTableNamesBySubStr(input) {
|
||||
if (this.props.offline || !this.props.queryEditor.dbId || !input) {
|
||||
return Promise.resolve({ options: [] });
|
||||
}
|
||||
|
||||
return SupersetClient.get({
|
||||
endpoint: `/superset/tables/${this.props.queryEditor.dbId}/${
|
||||
this.props.queryEditor.schema
|
||||
}/${input}`,
|
||||
}).then(({ json }) => ({ options: json.options }));
|
||||
onTableChange(tableName, schemaName) {
|
||||
this.props.actions.addTable(this.props.queryEditor, tableName, schemaName);
|
||||
}
|
||||
getDbList(dbs) {
|
||||
this.props.actions.setDatabases(dbs);
|
||||
}
|
||||
|
||||
dbMutator(data) {
|
||||
|
|
@ -76,34 +61,6 @@ class SqlEditorLeftBar extends React.PureComponent {
|
|||
resetState() {
|
||||
this.props.actions.resetState();
|
||||
}
|
||||
|
||||
fetchTables(dbId, schema, force, substr) {
|
||||
// This can be large so it shouldn't be put in the Redux store
|
||||
const forceRefresh = force || false;
|
||||
if (!this.props.offline && dbId && schema) {
|
||||
this.setState(() => ({ tableLoading: true, tableOptions: [] }));
|
||||
const endpoint = `/superset/tables/${dbId}/${schema}/${substr}/${forceRefresh}/`;
|
||||
|
||||
return SupersetClient.get({ endpoint })
|
||||
.then(({ json }) => {
|
||||
const filterOptions = createFilterOptions({ options: json.options });
|
||||
this.setState(() => ({
|
||||
filterOptions,
|
||||
tableLoading: false,
|
||||
tableOptions: json.options,
|
||||
tableLength: json.tableLength,
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState(() => ({ tableLoading: false, tableOptions: [], tableLength: 0 }));
|
||||
this.props.actions.addDangerToast(t('Error while fetching table list'));
|
||||
});
|
||||
}
|
||||
|
||||
this.setState(() => ({ tableLoading: false, tableOptions: [], filterOptions: null }));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
changeTable(tableOpt) {
|
||||
if (!tableOpt) {
|
||||
this.setState({ tableName: '' });
|
||||
|
|
@ -119,156 +76,30 @@ class SqlEditorLeftBar extends React.PureComponent {
|
|||
tableName = namePieces[1];
|
||||
this.setState({ tableName });
|
||||
this.props.actions.queryEditorSetSchema(this.props.queryEditor, schemaName);
|
||||
this.fetchTables(this.props.queryEditor.dbId, schemaName);
|
||||
}
|
||||
this.props.actions.addTable(this.props.queryEditor, tableName, schemaName);
|
||||
}
|
||||
|
||||
changeSchema(schemaOpt, force) {
|
||||
const schema = schemaOpt ? schemaOpt.value : null;
|
||||
this.props.actions.queryEditorSetSchema(this.props.queryEditor, schema);
|
||||
this.fetchTables(this.props.queryEditor.dbId, schema, force);
|
||||
}
|
||||
|
||||
fetchSchemas(dbId, force) {
|
||||
const actualDbId = dbId || this.props.queryEditor.dbId;
|
||||
const forceRefresh = force || false;
|
||||
if (!this.props.offline && actualDbId) {
|
||||
this.setState({ schemaLoading: true });
|
||||
const endpoint = `/superset/schemas/${actualDbId}/${forceRefresh}/`;
|
||||
|
||||
return SupersetClient.get({ endpoint })
|
||||
.then(({ json }) => {
|
||||
const schemaOptions = json.schemas.map(s => ({ value: s, label: s }));
|
||||
this.setState({ schemaOptions, schemaLoading: false });
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({ schemaLoading: false, schemaOptions: [] });
|
||||
this.props.actions.addDangerToast(t('Error while fetching schema list'));
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
closePopover(ref) {
|
||||
this.refs[ref].hide();
|
||||
}
|
||||
|
||||
render() {
|
||||
const shouldShowReset = window.location.search === '?reset=1';
|
||||
const tableMetaDataHeight = this.props.height - 130; // 130 is the height of the selects above
|
||||
let tableSelectPlaceholder;
|
||||
let tableSelectDisabled = false;
|
||||
if (this.props.database && this.props.database.allow_multi_schema_metadata_fetch) {
|
||||
tableSelectPlaceholder = t('Type to search ...');
|
||||
} else {
|
||||
tableSelectPlaceholder = t('Select table ');
|
||||
tableSelectDisabled = true;
|
||||
}
|
||||
const database = this.props.database || {};
|
||||
const qe = this.props.queryEditor;
|
||||
return (
|
||||
<div className="clearfix">
|
||||
<div>
|
||||
<AsyncSelect
|
||||
dataEndpoint={
|
||||
'/databaseasync/api/' +
|
||||
'read?_flt_0_expose_in_sqllab=1&' +
|
||||
'_oc_DatabaseAsync=database_name&' +
|
||||
'_od_DatabaseAsync=asc'
|
||||
}
|
||||
onChange={this.onDatabaseChange.bind(this)}
|
||||
onAsyncError={() => {
|
||||
this.props.actions.addDangerToast(t('Error while fetching database list'));
|
||||
}}
|
||||
value={this.props.queryEditor.dbId}
|
||||
databaseId={this.props.queryEditor.dbId}
|
||||
actions={this.props.actions}
|
||||
valueRenderer={o => (
|
||||
<div>
|
||||
<span className="text-muted">{t('Database:')}</span> {o.label}
|
||||
</div>
|
||||
)}
|
||||
mutator={this.dbMutator.bind(this)}
|
||||
placeholder={t('Select a database')}
|
||||
autoSelect
|
||||
/>
|
||||
</div>
|
||||
<div className="m-t-5">
|
||||
<div className="row">
|
||||
<div className="col-md-11 col-xs-11" style={{ paddingRight: '2px' }}>
|
||||
<Select
|
||||
name="select-schema"
|
||||
placeholder={t('Select a schema (%s)', this.state.schemaOptions.length)}
|
||||
options={this.state.schemaOptions}
|
||||
value={this.props.queryEditor.schema}
|
||||
valueRenderer={o => (
|
||||
<div>
|
||||
<span className="text-muted">{t('Schema:')}</span> {o.label}
|
||||
</div>
|
||||
)}
|
||||
isLoading={this.state.schemaLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeSchema.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-1 col-xs-1" style={{ paddingTop: '8px', paddingLeft: '0px' }}>
|
||||
<RefreshLabel
|
||||
onClick={this.onDatabaseChange.bind(
|
||||
this, { value: database.id }, true)}
|
||||
tooltipContent={t('force refresh schema list')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="m-t-5">
|
||||
<ControlLabel>
|
||||
{t('See table schema')}
|
||||
|
||||
<small>
|
||||
({this.state.tableOptions.length}
|
||||
|
||||
{t('in')}
|
||||
|
||||
<i>{this.props.queryEditor.schema}</i>)
|
||||
</small>
|
||||
</ControlLabel>
|
||||
<div className="row">
|
||||
<div className="col-md-11 col-xs-11" style={{ paddingRight: '2px' }}>
|
||||
{this.props.queryEditor.schema ? (
|
||||
<Select
|
||||
name="select-table"
|
||||
ref="selectTable"
|
||||
isLoading={this.state.tableLoading}
|
||||
placeholder={t('Select table or type table name')}
|
||||
autosize={false}
|
||||
onChange={this.changeTable.bind(this)}
|
||||
filterOptions={this.state.filterOptions}
|
||||
options={this.state.tableOptions}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
async
|
||||
name="async-select-table"
|
||||
ref="selectTable"
|
||||
placeholder={tableSelectPlaceholder}
|
||||
disabled={tableSelectDisabled}
|
||||
autosize={false}
|
||||
onChange={this.changeTable.bind(this)}
|
||||
loadOptions={this.getTableNamesBySubStr.bind(this)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-md-1 col-xs-1" style={{ paddingTop: '8px', paddingLeft: '0px' }}>
|
||||
<RefreshLabel
|
||||
onClick={this.changeSchema.bind(
|
||||
this, { value: this.props.queryEditor.schema }, true)}
|
||||
tooltipContent={t('force refresh table list')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TableSelector
|
||||
dbId={qe.dbId}
|
||||
schema={qe.schema}
|
||||
onDbChange={this.onDbChange}
|
||||
onSchemaChange={this.onSchemaChange}
|
||||
getDbList={this.getDbList}
|
||||
onTableChange={this.onTableChange}
|
||||
tableNameSticky={false}
|
||||
database={this.props.database}
|
||||
handleError={this.props.actions.addDangerToast}
|
||||
/>
|
||||
<hr />
|
||||
<div className="m-t-5">
|
||||
<div className="scrollbar-container">
|
||||
|
|
@ -279,23 +110,14 @@ class SqlEditorLeftBar extends React.PureComponent {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{shouldShowReset && (
|
||||
<Button bsSize="small" bsStyle="danger" onClick={this.resetState.bind(this)}>
|
||||
{shouldShowReset &&
|
||||
<Button bsSize="small" bsStyle="danger" onClick={this.resetState}>
|
||||
<i className="fa fa-bomb" /> {t('Reset State')}
|
||||
</Button>
|
||||
)}
|
||||
</Button>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps({ sqlLab }) {
|
||||
return {
|
||||
offline: sqlLab.offline,
|
||||
};
|
||||
}
|
||||
|
||||
SqlEditorLeftBar.propTypes = propTypes;
|
||||
SqlEditorLeftBar.defaultProps = defaultProps;
|
||||
|
||||
export default connect(mapStateToProps)(SqlEditorLeftBar);
|
||||
|
|
|
|||
|
|
@ -14,14 +14,12 @@ const propTypes = {
|
|||
PropTypes.number,
|
||||
PropTypes.arrayOf(PropTypes.number),
|
||||
]),
|
||||
valueRenderer: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
autoSelect: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
placeholder: t('Select ...'),
|
||||
valueRenderer: o => (<div>{o.label}</div>),
|
||||
onAsyncError: () => {},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,321 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'react-virtualized-select';
|
||||
import createFilterOptions from 'react-select-fast-filter-options';
|
||||
import { ControlLabel, Col, Label } from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { SupersetClient } from '@superset-ui/connection';
|
||||
|
||||
import AsyncSelect from './AsyncSelect';
|
||||
import RefreshLabel from './RefreshLabel';
|
||||
|
||||
const propTypes = {
|
||||
dbId: PropTypes.number.isRequired,
|
||||
schema: PropTypes.string,
|
||||
onSchemaChange: PropTypes.func,
|
||||
onDbChange: PropTypes.func,
|
||||
getDbList: PropTypes.func,
|
||||
onTableChange: PropTypes.func,
|
||||
tableNameSticky: PropTypes.bool,
|
||||
tableName: PropTypes.string,
|
||||
database: PropTypes.object,
|
||||
horizontal: PropTypes.bool,
|
||||
sqlLabMode: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
clearable: PropTypes.bool,
|
||||
handleError: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onDbChange: () => {},
|
||||
onSchemaChange: () => {},
|
||||
getDbList: () => {},
|
||||
onTableChange: () => {},
|
||||
onChange: () => {},
|
||||
tableNameSticky: true,
|
||||
horizontal: false,
|
||||
sqlLabMode: true,
|
||||
clearable: true,
|
||||
};
|
||||
|
||||
export default class TableSelector extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
schemaLoading: false,
|
||||
schemaOptions: [],
|
||||
tableLoading: false,
|
||||
tableOptions: [],
|
||||
dbId: props.dbId,
|
||||
schema: props.schema,
|
||||
tableName: props.tableName,
|
||||
filterOptions: null,
|
||||
};
|
||||
this.changeSchema = this.changeSchema.bind(this);
|
||||
this.changeTable = this.changeTable.bind(this);
|
||||
this.dbMutator = this.dbMutator.bind(this);
|
||||
this.getTableNamesBySubStr = this.getTableNamesBySubStr.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onDatabaseChange = this.onDatabaseChange.bind(this);
|
||||
}
|
||||
componentDidMount() {
|
||||
this.fetchSchemas(this.state.dbId);
|
||||
this.fetchTables();
|
||||
}
|
||||
onDatabaseChange(db, force = false) {
|
||||
const dbId = db ? db.id : null;
|
||||
this.setState({ schemaOptions: [] });
|
||||
this.props.onSchemaChange(null);
|
||||
this.props.onDbChange(db);
|
||||
this.fetchSchemas(dbId, force);
|
||||
this.setState({ dbId, schema: null, tableOptions: [] }, this.onChange);
|
||||
}
|
||||
onChange() {
|
||||
this.props.onChange({
|
||||
dbId: this.state.dbId,
|
||||
shema: this.state.schema,
|
||||
tableName: this.state.tableName,
|
||||
});
|
||||
}
|
||||
getTableNamesBySubStr(input) {
|
||||
const { tableName } = this.state;
|
||||
if (!this.props.dbId || !input) {
|
||||
const options = this.addOptionIfMissing([], tableName);
|
||||
return Promise.resolve({ options });
|
||||
}
|
||||
return SupersetClient.get({
|
||||
endpoint: (
|
||||
`/superset/tables/${this.props.dbId}/` +
|
||||
`${this.props.schema}/${input}`),
|
||||
}).then(({ json }) => ({ options: this.addOptionIfMissing(json.options, tableName) }));
|
||||
}
|
||||
dbMutator(data) {
|
||||
this.props.getDbList(data.result);
|
||||
if (data.result.length === 0) {
|
||||
this.props.handleError(t("It seems you don't have access to any database"));
|
||||
}
|
||||
return data.result;
|
||||
}
|
||||
fetchTables(force, substr) {
|
||||
// This can be large so it shouldn't be put in the Redux store
|
||||
const forceRefresh = force || false;
|
||||
const { dbId, schema } = this.props;
|
||||
if (dbId && schema) {
|
||||
this.setState(() => ({ tableLoading: true, tableOptions: [] }));
|
||||
const endpoint = `/superset/tables/${dbId}/${schema}/${substr}/${forceRefresh}/`;
|
||||
return SupersetClient.get({ endpoint })
|
||||
.then(({ json }) => {
|
||||
const filterOptions = createFilterOptions({ options: json.options });
|
||||
this.setState(() => ({
|
||||
filterOptions,
|
||||
tableLoading: false,
|
||||
tableOptions: json.options,
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState(() => ({ tableLoading: false, tableOptions: [] }));
|
||||
this.props.handleError(t('Error while fetching table list'));
|
||||
});
|
||||
}
|
||||
this.setState(() => ({ tableLoading: false, tableOptions: [], filterOptions: null }));
|
||||
return Promise.resolve();
|
||||
}
|
||||
fetchSchemas(dbId, force) {
|
||||
const actualDbId = dbId || this.props.dbId;
|
||||
const forceRefresh = force || false;
|
||||
if (actualDbId) {
|
||||
this.setState({ schemaLoading: true });
|
||||
const endpoint = `/superset/schemas/${actualDbId}/${forceRefresh}/`;
|
||||
|
||||
return SupersetClient.get({ endpoint })
|
||||
.then(({ json }) => {
|
||||
const schemaOptions = json.schemas.map(s => ({ value: s, label: s }));
|
||||
this.setState({ schemaOptions, schemaLoading: false });
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({ schemaLoading: false, schemaOptions: [] });
|
||||
this.props.handleError(t('Error while fetching schema list'));
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
changeTable(tableOpt) {
|
||||
if (!tableOpt) {
|
||||
this.setState({ tableName: '' });
|
||||
return;
|
||||
}
|
||||
const namePieces = tableOpt.value.split('.');
|
||||
let tableName = namePieces[0];
|
||||
let schemaName = this.props.schema;
|
||||
if (namePieces.length > 1) {
|
||||
schemaName = namePieces[0];
|
||||
tableName = namePieces[1];
|
||||
}
|
||||
if (this.props.tableNameSticky) {
|
||||
this.setState({ tableName }, this.onChange);
|
||||
}
|
||||
this.props.onTableChange(tableName, schemaName);
|
||||
}
|
||||
changeSchema(schemaOpt) {
|
||||
const schema = schemaOpt ? schemaOpt.value : null;
|
||||
this.props.onSchemaChange(schema);
|
||||
this.setState({ schema }, () => {
|
||||
this.fetchTables();
|
||||
this.onChange();
|
||||
});
|
||||
}
|
||||
addOptionIfMissing(options, value) {
|
||||
if (options.filter(o => o.value === this.state.tableName).length === 0 && value) {
|
||||
return [...options, { value, label: value }];
|
||||
}
|
||||
return options;
|
||||
}
|
||||
renderDatabaseOption(db) {
|
||||
return (
|
||||
<span>
|
||||
<Label bsStyle="default" className="m-r-5">{db.backend}</Label>
|
||||
{db.database_name}
|
||||
</span>);
|
||||
}
|
||||
renderDatabaseSelect() {
|
||||
return (
|
||||
<AsyncSelect
|
||||
dataEndpoint={
|
||||
'/databaseasync/api/' +
|
||||
'read?_flt_0_expose_in_sqllab=1&' +
|
||||
'_oc_DatabaseAsync=database_name&' +
|
||||
'_od_DatabaseAsync=asc'
|
||||
}
|
||||
onChange={this.onDatabaseChange}
|
||||
onAsyncError={() => this.props.handleError(t('Error while fetching database list'))}
|
||||
clearable={false}
|
||||
value={this.state.dbId}
|
||||
valueKey="id"
|
||||
valueRenderer={db => (
|
||||
<div>
|
||||
<span className="text-muted m-r-5">{t('Database:')}</span>
|
||||
{this.renderDatabaseOption(db)}
|
||||
</div>
|
||||
)}
|
||||
optionRenderer={this.renderDatabaseOption}
|
||||
mutator={this.dbMutator}
|
||||
placeholder={t('Select a database')}
|
||||
autoSelect
|
||||
/>);
|
||||
}
|
||||
renderSchema() {
|
||||
return (
|
||||
<div className="m-t-5">
|
||||
<div className="row">
|
||||
<div className="col-md-11 col-xs-11 p-r-2">
|
||||
<Select
|
||||
name="select-schema"
|
||||
placeholder={t('Select a schema (%s)', this.state.schemaOptions.length)}
|
||||
options={this.state.schemaOptions}
|
||||
value={this.props.schema}
|
||||
valueRenderer={o => (
|
||||
<div>
|
||||
<span className="text-muted">{t('Schema:')}</span> {o.label}
|
||||
</div>
|
||||
)}
|
||||
isLoading={this.state.schemaLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeSchema}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-1 col-xs-1 p-l-0 p-t-8">
|
||||
<RefreshLabel
|
||||
onClick={() => this.onDatabaseChange({ id: this.props.dbId }, true)}
|
||||
tooltipContent={t('force refresh schema list')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
renderTable() {
|
||||
let tableSelectPlaceholder;
|
||||
let tableSelectDisabled = false;
|
||||
if (this.props.database && this.props.database.allow_multi_schema_metadata_fetch) {
|
||||
tableSelectPlaceholder = t('Type to search ...');
|
||||
} else {
|
||||
tableSelectPlaceholder = t('Select table ');
|
||||
tableSelectDisabled = true;
|
||||
}
|
||||
const options = this.addOptionIfMissing(this.state.tableOptions, this.state.tableName);
|
||||
return (
|
||||
<div className="m-t-5">
|
||||
<div className="row">
|
||||
<div className="col-md-11 col-xs-11 p-r-2">
|
||||
{this.props.schema ? (
|
||||
<Select
|
||||
name="select-table"
|
||||
ref="selectTable"
|
||||
isLoading={this.state.tableLoading}
|
||||
placeholder={t('Select table or type table name')}
|
||||
autosize={false}
|
||||
onChange={this.changeTable}
|
||||
filterOptions={this.state.filterOptions}
|
||||
options={options}
|
||||
value={this.state.tableName}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
async
|
||||
name="async-select-table"
|
||||
ref="selectTable"
|
||||
placeholder={tableSelectPlaceholder}
|
||||
disabled={tableSelectDisabled}
|
||||
autosize={false}
|
||||
onChange={this.changeTable}
|
||||
value={this.state.tableName}
|
||||
loadOptions={this.getTableNamesBySubStr}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-md-1 col-xs-1 p-l-0 p-t-8">
|
||||
<RefreshLabel
|
||||
onClick={() => this.changeSchema({ value: this.props.schema }, true)}
|
||||
tooltipContent={t('force refresh table list')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
renderSeeTableLabel() {
|
||||
return (
|
||||
<div>
|
||||
<hr />
|
||||
<ControlLabel>
|
||||
{t('See table schema')}{' '}
|
||||
<small>
|
||||
({this.state.tableOptions.length}
|
||||
{' '}{t('in')}{' '}
|
||||
<i>
|
||||
{this.props.schema}
|
||||
</i>)
|
||||
</small>
|
||||
</ControlLabel>
|
||||
</div>);
|
||||
}
|
||||
render() {
|
||||
if (this.props.horizontal) {
|
||||
return (
|
||||
<div>
|
||||
<Col md={4}>{this.renderDatabaseSelect()}</Col>
|
||||
<Col md={4}>{this.renderSchema()}</Col>
|
||||
<Col md={4}>{this.renderTable()}</Col>
|
||||
</div>);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div>{this.renderDatabaseSelect()}</div>
|
||||
<div className="m-t-5">{this.renderSchema()}</div>
|
||||
{this.props.sqlLabMode && this.renderSeeTableLabel()}
|
||||
<div className="m-t-5">{this.renderTable()}</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
TableSelector.propTypes = propTypes;
|
||||
TableSelector.defaultProps = defaultProps;
|
||||
|
|
@ -8,6 +8,7 @@ import getClientErrorObject from '../utils/getClientErrorObject';
|
|||
|
||||
import Button from '../components/Button';
|
||||
import Loading from '../components/Loading';
|
||||
import TableSelector from '../components/TableSelector';
|
||||
import CheckboxControl from '../explore/components/controls/CheckboxControl';
|
||||
import TextControl from '../explore/components/controls/TextControl';
|
||||
import SelectControl from '../explore/components/controls/SelectControl';
|
||||
|
|
@ -219,9 +220,8 @@ export class DatasourceEditor extends React.PureComponent {
|
|||
};
|
||||
this.props.onChange(datasource, this.state.errors);
|
||||
}
|
||||
|
||||
onDatasourceChange(newDatasource) {
|
||||
this.setState({ datasource: newDatasource }, this.validateAndChange);
|
||||
onDatasourceChange(datasource) {
|
||||
this.setState({ datasource }, this.validateAndChange);
|
||||
}
|
||||
|
||||
onDatasourcePropChange(attr, value) {
|
||||
|
|
@ -260,11 +260,15 @@ export class DatasourceEditor extends React.PureComponent {
|
|||
}
|
||||
syncMetadata() {
|
||||
const { datasource } = this.state;
|
||||
const endpoint = (
|
||||
`/datasource/external_metadata/${datasource.type}/${datasource.id}/` +
|
||||
`?db_id=${datasource.database.id}` +
|
||||
`&schema=${datasource.schema}` +
|
||||
`&table_name=${datasource.datasource_name}`
|
||||
);
|
||||
this.setState({ metadataLoading: true });
|
||||
|
||||
SupersetClient.get({
|
||||
endpoint: `/datasource/external_metadata/${datasource.type}/${datasource.id}/`,
|
||||
}).then(({ json }) => {
|
||||
SupersetClient.get({ endpoint }).then(({ json }) => {
|
||||
this.mergeColumns(json);
|
||||
this.props.addSuccessToast(t('Metadata has been synced'));
|
||||
this.setState({ metadataLoading: false });
|
||||
|
|
@ -319,6 +323,27 @@ export class DatasourceEditor extends React.PureComponent {
|
|||
const datasource = this.state.datasource;
|
||||
return (
|
||||
<Fieldset title={t('Basic')} item={datasource} onChange={this.onDatasourceChange}>
|
||||
{this.state.isSqla &&
|
||||
<Field
|
||||
fieldKey="tableSelector"
|
||||
label={t('Physical Table')}
|
||||
control={
|
||||
<TableSelector
|
||||
dbId={datasource.database.id}
|
||||
schema={datasource.schema}
|
||||
tableName={datasource.datasource_name}
|
||||
onSchemaChange={schema => this.onDatasourcePropChange('schema', schema)}
|
||||
onDbChange={database => this.onDatasourcePropChange('database', database)}
|
||||
onTableChange={table => this.onDatasourcePropChange('datasource_name', table)}
|
||||
sqlLabMode={false}
|
||||
clearable={false}
|
||||
handleError={this.props.addDangerToast}
|
||||
/>}
|
||||
descr={t(
|
||||
'The pointer to a physical table. Keep in mind that the chart is ' +
|
||||
'associated to this Superset logical table, and this logical table points ' +
|
||||
'the physical table referenced here.')}
|
||||
/>}
|
||||
<Field
|
||||
fieldKey="description"
|
||||
label={t('Description')}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,18 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Col,
|
||||
Collapse,
|
||||
Label,
|
||||
OverlayTrigger,
|
||||
Row,
|
||||
Tooltip,
|
||||
Well,
|
||||
} from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import DatasourceModal from '../../../datasource/DatasourceModal';
|
||||
import ColumnOption from '../../../components/ColumnOption';
|
||||
import MetricOption from '../../../components/MetricOption';
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string.isRequired,
|
||||
value: PropTypes.string,
|
||||
datasource: PropTypes.object.isRequired,
|
||||
onDatasourceSave: PropTypes.func,
|
||||
};
|
||||
|
|
@ -26,6 +20,7 @@ const propTypes = {
|
|||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
onDatasourceSave: () => {},
|
||||
value: null,
|
||||
};
|
||||
|
||||
class DatasourceControl extends React.PureComponent {
|
||||
|
|
@ -58,41 +53,6 @@ class DatasourceControl extends React.PureComponent {
|
|||
showEditDatasourceModal: !showEditDatasourceModal,
|
||||
}));
|
||||
}
|
||||
|
||||
renderDatasource() {
|
||||
const datasource = this.props.datasource;
|
||||
return (
|
||||
<div className="m-t-10">
|
||||
<Well className="m-t-0">
|
||||
<div className="m-b-10">
|
||||
<Label>
|
||||
<i className="fa fa-database" /> {datasource.database.backend}
|
||||
</Label>
|
||||
{` ${datasource.database.name} `}
|
||||
</div>
|
||||
<Row className="datasource-container">
|
||||
<Col md={6}>
|
||||
<strong>Columns</strong>
|
||||
{datasource.columns.map(col => (
|
||||
<div key={col.column_name}>
|
||||
<ColumnOption showType column={col} />
|
||||
</div>
|
||||
))}
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
<strong>Metrics</strong>
|
||||
{datasource.metrics.map(m => (
|
||||
<div key={m.metric_name}>
|
||||
<MetricOption metric={m} showType />
|
||||
</div>
|
||||
))}
|
||||
</Col>
|
||||
</Row>
|
||||
</Well>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -107,21 +67,6 @@ class DatasourceControl extends React.PureComponent {
|
|||
{this.props.datasource.name}
|
||||
</Label>
|
||||
</OverlayTrigger>
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={
|
||||
<Tooltip id={'toggle-datasource-tooltip'}>
|
||||
{t('Expand/collapse datasource configuration')}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<a href="#">
|
||||
<i
|
||||
className={`fa fa-${this.state.showDatasource ? 'minus' : 'plus'}-square m-r-5`}
|
||||
onClick={this.toggleShowDatasource}
|
||||
/>
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
{this.props.datasource.type === 'table' &&
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
|
|
@ -139,7 +84,6 @@ class DatasourceControl extends React.PureComponent {
|
|||
<i className="fa fa-flask m-r-5" />
|
||||
</a>
|
||||
</OverlayTrigger>}
|
||||
<Collapse in={this.state.showDatasource}>{this.renderDatasource()}</Collapse>
|
||||
<DatasourceModal
|
||||
datasource={this.props.datasource}
|
||||
show={this.state.showEditDatasourceModal}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class DashboardTable extends React.PureComponent {
|
|||
{this.state.dashboards.map(o => (
|
||||
<Tr key={o.id}>
|
||||
<Td column="dashboard" value={o.dashboard_title}>
|
||||
<a href={o.url}>{o.dashboard_title}</a>
|
||||
{o.dashboard_title}
|
||||
</Td>
|
||||
<Td column="creator" value={o.changed_by_name}>
|
||||
{unsafe(o.creator)}
|
||||
|
|
|
|||
|
|
@ -278,6 +278,16 @@ table.table-no-hover tr:hover {
|
|||
.m-l-25 {
|
||||
margin-left: 25px;
|
||||
}
|
||||
.p-l-0 {
|
||||
padding-left: 0;
|
||||
}
|
||||
.p-t-8 {
|
||||
padding-top: 8;
|
||||
}
|
||||
.p-r-2 {
|
||||
padding-right: 2;
|
||||
}
|
||||
|
||||
.Select-menu-outer {
|
||||
z-index: 10 !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ class DatabaseAsync(DatabaseView):
|
|||
'expose_in_sqllab', 'allow_ctas', 'force_ctas_schema',
|
||||
'allow_run_async', 'allow_dml',
|
||||
'allow_multi_schema_metadata_fetch', 'allow_csv_upload',
|
||||
'allows_subquery',
|
||||
'allows_subquery', 'backend',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from flask_babel import gettext as __
|
|||
|
||||
from superset import appbuilder, db
|
||||
from superset.connectors.connector_registry import ConnectorRegistry
|
||||
from superset.models.core import Database
|
||||
from .base import BaseSupersetView, check_ownership, json_error_response
|
||||
|
||||
|
||||
|
|
@ -42,9 +43,24 @@ class Datasource(BaseSupersetView):
|
|||
@has_access_api
|
||||
def external_metadata(self, datasource_type=None, datasource_id=None):
|
||||
"""Gets column info from the source system"""
|
||||
orm_datasource = ConnectorRegistry.get_datasource(
|
||||
datasource_type, datasource_id, db.session)
|
||||
return self.json_response(orm_datasource.external_metadata())
|
||||
if datasource_type == 'druid':
|
||||
datasource = ConnectorRegistry.get_datasource(
|
||||
datasource_type, datasource_id, db.session)
|
||||
elif datasource_type == 'table':
|
||||
database = (
|
||||
db.session
|
||||
.query(Database)
|
||||
.filter_by(id=request.args.get('db_id'))
|
||||
.one()
|
||||
)
|
||||
Table = ConnectorRegistry.sources['table']
|
||||
datasource = Table(
|
||||
database=database,
|
||||
table_name=request.args.get('table_name'),
|
||||
schema=request.args.get('schema') or None,
|
||||
)
|
||||
external_metadata = datasource.external_metadata()
|
||||
return self.json_response(external_metadata)
|
||||
|
||||
|
||||
appbuilder.add_view_no_menu(Datasource)
|
||||
|
|
|
|||
|
|
@ -12,8 +12,14 @@ class DatasourceTests(SupersetTestCase):
|
|||
|
||||
def test_external_metadata(self):
|
||||
self.login(username='admin')
|
||||
tbl_id = self.get_table_by_name('birth_names').id
|
||||
url = '/datasource/external_metadata/table/{}/'.format(tbl_id)
|
||||
tbl = self.get_table_by_name('birth_names')
|
||||
schema = tbl.schema or ''
|
||||
url = (
|
||||
f'/datasource/external_metadata/table/{tbl.id}/?'
|
||||
f'db_id={tbl.database.id}&'
|
||||
f'table_name={tbl.table_name}&'
|
||||
f'schema={schema}&'
|
||||
)
|
||||
resp = self.get_json_resp(url)
|
||||
col_names = {o.get('name') for o in resp}
|
||||
self.assertEquals(
|
||||
|
|
|
|||
Loading…
Reference in New Issue