From b39841f04181a392e8f337651111f855472bcba0 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Tue, 23 Jan 2018 17:51:04 -0800 Subject: [PATCH 1/2] [table editor] allow selecting physical table --- .../components/TableSelector_spec.jsx | 201 +++++++++++ .../sqllab/SqlEditorLeftBar_spec.jsx | 190 +---------- .../spec/javascripts/sqllab/fixtures.js | 1 - .../SqlLab/components/SqlEditorLeftBar.jsx | 242 ++----------- .../assets/src/components/AsyncSelect.jsx | 2 - .../assets/src/components/TableSelector.jsx | 321 ++++++++++++++++++ .../src/datasource/DatasourceEditor.jsx | 37 +- .../components/controls/DatasourceControl.jsx | 60 +--- .../assets/src/welcome/DashboardTable.jsx | 2 +- superset/views/core.py | 2 +- superset/views/datasource.py | 22 +- tests/datasource_tests.py | 10 +- 12 files changed, 618 insertions(+), 472 deletions(-) create mode 100644 superset/assets/spec/javascripts/components/TableSelector_spec.jsx create mode 100644 superset/assets/src/components/TableSelector.jsx 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 0000000000000..c4d07a167260a --- /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(, { + context: { store }, + }); + inst = wrapper.instance(); + }); + + it('is valid', () => { + expect(React.isValidElement()).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 9d3c3f64a807d..19596220ce45e 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(, { 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 9bcbdfe28cebf..77d03aa8f4e0d 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 933afa4704e36..4c722782b29ce 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 (
-
- { - 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 => ( -
- {t('Database:')} {o.label} -
- )} - mutator={this.dbMutator.bind(this)} - placeholder={t('Select a database')} - autoSelect - /> -
-
-
-
- - ) : ( - ( +
+ {t('Schema:')} {o.label} +
+ )} + isLoading={this.state.schemaLoading} + autosize={false} + onChange={this.changeSchema} + /> +
+
+ this.onDatabaseChange({ id: this.props.dbId }, true)} + tooltipContent={t('force refresh schema list')} + /> +
+
+
+ ); + } + 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 ( +
+
+
+ {this.props.schema ? ( + + )} +
+
+ this.changeSchema({ value: this.props.schema }, true)} + tooltipContent={t('force refresh table list')} + /> +
+
+
); + } + renderSeeTableLabel() { + return ( +
+
+ + {t('See table schema')}{' '} + + ({this.state.tableOptions.length} + {' '}{t('in')}{' '} + + {this.props.schema} + ) + + +
); + } + render() { + if (this.props.horizontal) { + return ( +
+ {this.renderDatabaseSelect()} + {this.renderSchema()} + {this.renderTable()} +
); + } + return ( +
+
{this.renderDatabaseSelect()}
+
{this.renderSchema()}
+ {this.props.sqlLabMode && this.renderSeeTableLabel()} +
{this.renderTable()}
+
); + } +} +TableSelector.propTypes = propTypes; +TableSelector.defaultProps = defaultProps; diff --git a/superset/assets/src/datasource/DatasourceEditor.jsx b/superset/assets/src/datasource/DatasourceEditor.jsx index 8df3fd9002ca1..b03bc0920efed 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 (
+ {this.state.isSqla && + 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.')} + />} {}, 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 ( -
- -
- - {` ${datasource.database.name} `} -
- - - Columns - {datasource.columns.map(col => ( -
- -
- ))} - - - Metrics - {datasource.metrics.map(m => ( -
- -
- ))} - -
-
-
- ); - } - render() { return (
@@ -107,21 +67,6 @@ class DatasourceControl extends React.PureComponent { {this.props.datasource.name} - - {t('Expand/collapse datasource configuration')} - - } - > - - - - {this.props.datasource.type === 'table' && } - {this.renderDatasource()} ( - {o.dashboard_title} + {o.dashboard_title} {unsafe(o.creator)} diff --git a/superset/views/core.py b/superset/views/core.py index e8b1859c1af0a..2c154e6eff94e 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 9d3d3419eab2c..74bd6ad70646f 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 64c57b55f3cdd..52674a20e7264 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( From cffb69e80929891acaf20dfd4d692fc47d14f0da Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Sun, 13 Jan 2019 17:58:34 -0800 Subject: [PATCH 2/2] Using classes for padding --- superset/assets/src/components/TableSelector.jsx | 8 ++++---- superset/assets/stylesheets/superset.less | 10 ++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/superset/assets/src/components/TableSelector.jsx b/superset/assets/src/components/TableSelector.jsx index 847b6de5fa92c..62e5e159dc257 100644 --- a/superset/assets/src/components/TableSelector.jsx +++ b/superset/assets/src/components/TableSelector.jsx @@ -208,7 +208,7 @@ export default class TableSelector extends React.PureComponent { return (
-
+
)}
-
+
this.changeSchema({ value: this.props.schema }, true)} tooltipContent={t('force refresh table list')} diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less index d7631ee43d61d..5e0e90ee43e1f 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; }