diff --git a/superset/assets/spec/javascripts/components/TableSelector_spec.jsx
b/superset/assets/spec/javascripts/components/TableSelector_spec.jsx
new file mode 100644
index 0000000000..c4d07a1672
--- /dev/null
+++ b/superset/assets/spec/javascripts/components/TableSelector_spec.jsx
@@ -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();
+ });
+});
diff --git a/superset/assets/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
b/superset/assets/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
index 9d3c3f64a8..19596220ce 100644
--- a/superset/assets/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
+++ b/superset/assets/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
@@ -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();
- });
});
diff --git a/superset/assets/spec/javascripts/sqllab/fixtures.js
b/superset/assets/spec/javascripts/sqllab/fixtures.js
index 9bcbdfe28c..77d03aa8f4 100644
--- a/superset/assets/spec/javascripts/sqllab/fixtures.js
+++ b/superset/assets/spec/javascripts/sqllab/fixtures.js
@@ -309,7 +309,6 @@ export const databases = {
],
};
export const tables = {
- tableLength: 3,
options: [
{
value: 'birth_names',
diff --git a/superset/assets/src/SqlLab/components/SqlEditorLeftBar.jsx
b/superset/assets/src/SqlLab/components/SqlEditorLeftBar.jsx
index 933afa4704..4c722782b2 100644
--- a/superset/assets/src/SqlLab/components/SqlEditorLeftBar.jsx
+++ b/superset/assets/src/SqlLab/components/SqlEditorLeftBar.jsx
@@ -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);
diff --git a/superset/assets/src/components/AsyncSelect.jsx
b/superset/assets/src/components/AsyncSelect.jsx
index 0784d2d6e6..240f4ad651 100644
--- a/superset/assets/src/components/AsyncSelect.jsx
+++ b/superset/assets/src/components/AsyncSelect.jsx
@@ -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: () => {},
};
diff --git a/superset/assets/src/components/TableSelector.jsx
b/superset/assets/src/components/TableSelector.jsx
new file mode 100644
index 0000000000..62e5e159dc
--- /dev/null
+++ b/superset/assets/src/components/TableSelector.jsx
@@ -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;
diff --git a/superset/assets/src/datasource/DatasourceEditor.jsx
b/superset/assets/src/datasource/DatasourceEditor.jsx
index 8df3fd9002..b03bc0920e 100644
--- a/superset/assets/src/datasource/DatasourceEditor.jsx
+++ b/superset/assets/src/datasource/DatasourceEditor.jsx
@@ -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')}
diff --git
a/superset/assets/src/explore/components/controls/DatasourceControl.jsx
b/superset/assets/src/explore/components/controls/DatasourceControl.jsx
index 03d1fbbb8f..023d740272 100644
--- a/superset/assets/src/explore/components/controls/DatasourceControl.jsx
+++ b/superset/assets/src/explore/components/controls/DatasourceControl.jsx
@@ -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}
diff --git a/superset/assets/src/welcome/DashboardTable.jsx
b/superset/assets/src/welcome/DashboardTable.jsx
index 80c06afb20..4789c0eabd 100644
--- a/superset/assets/src/welcome/DashboardTable.jsx
+++ b/superset/assets/src/welcome/DashboardTable.jsx
@@ -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)}
diff --git a/superset/assets/stylesheets/superset.less
b/superset/assets/stylesheets/superset.less
index d7631ee43d..5e0e90ee43 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -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;
}
diff --git a/superset/views/core.py b/superset/views/core.py
index e8b1859c1a..2c154e6eff 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -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',
]
diff --git a/superset/views/datasource.py b/superset/views/datasource.py
index 9d3d3419ea..74bd6ad706 100644
--- a/superset/views/datasource.py
+++ b/superset/views/datasource.py
@@ -8,6 +8,7 @@
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 @@ def save(self):
@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)
diff --git a/tests/datasource_tests.py b/tests/datasource_tests.py
index 64c57b55f3..52674a20e7 100644
--- a/tests/datasource_tests.py
+++ b/tests/datasource_tests.py
@@ -12,8 +12,14 @@ def __init__(self, *args, **kwargs):
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(
With regards,
Apache Git Services