superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From GitBox <...@apache.org>
Subject [incubator-superset] Diff for: [GitHub] mistercrunch merged pull request #6046: [table editor] allow selecting physical table
Date Tue, 15 Jan 2019 16:53:27 GMT
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')}
-            &nbsp;
-            <small>
-              ({this.state.tableOptions.length}
-              &nbsp;
-              {t('in')}
-              &nbsp;
-              <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

Mime
View raw message