You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ta...@apache.org on 2020/09/28 18:16:22 UTC
[incubator-superset] branch master updated: refactor: table
selector on dataset editor (#10914)
This is an automated email from the ASF dual-hosted git repository.
tai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new e337355 refactor: table selector on dataset editor (#10914)
e337355 is described below
commit e337355162f3f5716f1cad79a6b51a1359fecac8
Author: Lily Kuang <li...@preset.io>
AuthorDate: Mon Sep 28 11:16:03 2020 -0700
refactor: table selector on dataset editor (#10914)
Co-authored-by: Maxime Beauchemin <ma...@gmail.com>
---
.../cypress/integration/explore/control.test.ts | 2 +
.../javascripts/components/TableSelector_spec.jsx | 430 ++++++++++----------
.../datasource/DatasourceModal_spec.jsx | 10 +-
.../src/SqlLab/components/SqlEditorLeftBar.jsx | 11 +-
.../src/components/DatabaseSelector.tsx | 283 +++++++++++++
superset-frontend/src/components/TableSelector.jsx | 446 ---------------------
superset-frontend/src/components/TableSelector.tsx | 379 +++++++++++++++++
.../src/datasource/DatasourceEditor.jsx | 209 +++++++---
.../views/CRUD/data/dataset/AddDatasetModal.tsx | 9 +-
superset-frontend/stylesheets/superset.less | 4 +
tests/datasets/api_tests.py | 4 -
11 files changed, 1050 insertions(+), 737 deletions(-)
diff --git a/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts
index 57908b9..a25a28d 100644
--- a/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts
@@ -48,6 +48,7 @@ describe('Datasource control', () => {
});
// create new metric
+ cy.get('a[role="tab"]').contains('Metrics').click();
cy.get('button').contains('Add Item', { timeout: 10000 }).click();
cy.get('input[value="<new metric>"]').click();
cy.get('input[value="<new metric>"]')
@@ -64,6 +65,7 @@ describe('Datasource control', () => {
// delete metric
cy.get('#datasource_menu').click();
cy.get('a').contains('Edit Datasource').click();
+ cy.get('a[role="tab"]').contains('Metrics').click();
cy.get(`input[value="${newMetricName}"]`)
.closest('tr')
.find('.fa-trash')
diff --git a/superset-frontend/spec/javascripts/components/TableSelector_spec.jsx b/superset-frontend/spec/javascripts/components/TableSelector_spec.jsx
index b78c214..96b1d2e 100644
--- a/superset-frontend/spec/javascripts/components/TableSelector_spec.jsx
+++ b/superset-frontend/spec/javascripts/components/TableSelector_spec.jsx
@@ -18,267 +18,273 @@
*/
import React from 'react';
import configureStore from 'redux-mock-store';
-import { shallow } from 'enzyme';
+import { mount } from 'enzyme';
+import { act } from 'react-dom/test-utils';
import sinon from 'sinon';
import fetchMock from 'fetch-mock';
import thunk from 'redux-thunk';
+import { supersetTheme, ThemeProvider } from '@superset-ui/core';
+import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
+
+import DatabaseSelector from 'src/components/DatabaseSelector';
import TableSelector from 'src/components/TableSelector';
import { initialState, tables } from '../sqllab/fixtures';
-describe('TableSelector', () => {
- let mockedProps;
- const middlewares = [thunk];
- const mockStore = configureStore(middlewares);
- const store = mockStore(initialState);
- let wrapper;
- let inst;
+const mockStore = configureStore([thunk]);
+const store = mockStore(initialState);
- 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();
- });
+const FETCH_SCHEMAS_ENDPOINT = 'glob:*/api/v1/database/*/schemas/*';
+const GET_TABLE_ENDPOINT = 'glob:*/superset/tables/1/*/*';
+const GET_TABLE_NAMES_ENDPOINT = 'glob:*/superset/tables/1/main/*';
- it('is valid', () => {
- expect(React.isValidElement(<TableSelector {...mockedProps} />)).toBe(true);
- });
+const mockedProps = {
+ clearable: false,
+ database: { id: 1, database_name: 'main' },
+ dbId: 1,
+ formMode: false,
+ getDbList: sinon.stub(),
+ handleError: sinon.stub(),
+ horizontal: false,
+ onChange: sinon.stub(),
+ onDbChange: sinon.stub(),
+ onSchemaChange: sinon.stub(),
+ onTableChange: sinon.stub(),
+ sqlLabMode: true,
+ tableName: '',
+ tableNameSticky: 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/*';
+const schemaOptions = {
+ result: ['main', 'erf', 'superset'],
+};
+const selectedSchema = { label: 'main', title: 'main', value: 'main' };
+const selectedTable = {
+ label: 'birth_names',
+ schema: 'main',
+ title: 'birth_names',
+ value: 'birth_names',
+ type: undefined,
+};
- afterEach(fetchMock.resetHistory);
- afterAll(fetchMock.reset);
-
- it('should handle empty', () =>
- inst.getTableNamesBySubStr('').then(data => {
- expect(data).toEqual({ options: [] });
- return Promise.resolve();
- }));
+async function mountAndWait(props = mockedProps) {
+ const mounted = mount(<TableSelector {...props} />, {
+ context: { store },
+ wrappingComponent: ThemeProvider,
+ wrappingComponentProps: { theme: supersetTheme },
+ });
+ await waitForComponentToPaint(mounted);
- it('should handle table name', () => {
- fetchMock.get(GET_TABLE_NAMES_GLOB, tables, { overwriteRoutes: true });
+ return mounted;
+}
- return wrapper
- .instance()
- .getTableNamesBySubStr('my table')
- .then(data => {
- expect(fetchMock.calls(GET_TABLE_NAMES_GLOB)).toHaveLength(1);
- expect(data).toEqual({
- options: [
- {
- value: 'birth_names',
- schema: 'main',
- label: 'birth_names',
- title: 'birth_names',
- },
- {
- value: 'energy_usage',
- schema: 'main',
- label: 'energy_usage',
- title: 'energy_usage',
- },
- {
- value: 'wb_health_population',
- schema: 'main',
- label: 'wb_health_population',
- title: 'wb_health_population',
- },
- ],
- });
- return Promise.resolve();
- });
- });
+describe('TableSelector', () => {
+ let wrapper;
- it('should escape schema and table names', () => {
- const GET_TABLE_GLOB = 'glob:*/superset/tables/1/*/*';
- wrapper.setProps({ schema: 'slashed/schema' });
- fetchMock.get(GET_TABLE_GLOB, tables, { overwriteRoutes: true });
+ beforeEach(async () => {
+ fetchMock.reset();
+ wrapper = await mountAndWait();
+ });
- return wrapper
- .instance()
- .getTableNamesBySubStr('slashed/table')
- .then(() => {
- expect(fetchMock.lastUrl(GET_TABLE_GLOB)).toContain(
- '/slashed%252Fschema/slashed%252Ftable',
- );
- return Promise.resolve();
- });
- });
+ it('renders', () => {
+ expect(wrapper.find(TableSelector)).toExist();
+ expect(wrapper.find(DatabaseSelector)).toExist();
});
- describe('fetchTables', () => {
- const FETCH_TABLES_GLOB = 'glob:*/superset/tables/1/main/*/*/';
+ describe('change database', () => {
afterEach(fetchMock.resetHistory);
afterAll(fetchMock.reset);
- it('should clear table options', () => {
- inst.fetchTables(true);
- expect(wrapper.state().tableOptions).toEqual([]);
+ it('should fetch schemas', async () => {
+ fetchMock.get(FETCH_SCHEMAS_ENDPOINT, { overwriteRoutes: true });
+ act(() => {
+ wrapper.find('[data-test="select-database"]').first().props().onChange({
+ id: 1,
+ database_name: 'main',
+ });
+ });
+ await waitForComponentToPaint(wrapper);
+ expect(fetchMock.calls(FETCH_SCHEMAS_ENDPOINT)).toHaveLength(1);
});
- it('should fetch table options', () => {
- fetchMock.get(FETCH_TABLES_GLOB, tables, { overwriteRoutes: true });
- return inst.fetchTables(true, 'birth_names').then(() => {
- expect(wrapper.state().tableOptions).toHaveLength(3);
- expect(wrapper.state().tableOptions).toEqual([
- {
- value: 'birth_names',
- schema: 'main',
- label: 'birth_names',
- title: 'birth_names',
- },
- {
- value: 'energy_usage',
- schema: 'main',
- label: 'energy_usage',
- title: 'energy_usage',
- },
- {
- value: 'wb_health_population',
- schema: 'main',
- label: 'wb_health_population',
- title: 'wb_health_population',
- },
- ]);
- return Promise.resolve();
+ it('should fetch schema options', async () => {
+ fetchMock.get(FETCH_SCHEMAS_ENDPOINT, schemaOptions, {
+ overwriteRoutes: true,
});
- });
+ act(() => {
+ wrapper.find('[data-test="select-database"]').first().props().onChange({
+ id: 1,
+ database_name: 'main',
+ });
+ });
+ await waitForComponentToPaint(wrapper);
+ wrapper.update();
+ expect(fetchMock.calls(FETCH_SCHEMAS_ENDPOINT)).toHaveLength(1);
- // Test needs to be fixed: Github issue #7768
- it.skip('should dispatch a danger toast on error', () => {
- fetchMock.get(
- FETCH_TABLES_GLOB,
- { throws: 'error' },
- { overwriteRoutes: true },
- );
+ expect(
+ wrapper.find('[name="select-schema"]').first().props().options,
+ ).toEqual([
+ { value: 'main', label: 'main', title: 'main' },
+ { value: 'erf', label: 'erf', title: 'erf' },
+ { value: 'superset', label: 'superset', title: 'superset' },
+ ]);
+ });
- wrapper
- .instance()
- .fetchTables(true, 'birth_names')
- .then(() => {
- expect(wrapper.state().tableOptions).toEqual([]);
- expect(wrapper.state().tableOptions).toHaveLength(0);
- expect(mockedProps.handleError.callCount).toBe(1);
- return Promise.resolve();
+ it('should clear table options', async () => {
+ act(() => {
+ wrapper.find('[data-test="select-database"]').first().props().onChange({
+ id: 1,
+ database_name: 'main',
});
+ });
+ await waitForComponentToPaint(wrapper);
+ const props = wrapper.find('[name="async-select-table"]').first().props();
+ expect(props.isDisabled).toBe(true);
+ expect(props.value).toEqual(undefined);
});
});
- describe('fetchSchemas', () => {
- const FETCH_SCHEMAS_GLOB = 'glob:*/api/v1/database/*/schemas/?q=(force:!*)';
- afterEach(fetchMock.resetHistory);
- afterAll(fetchMock.reset);
-
- it('should fetch schema options', () => {
- const schemaOptions = {
- result: ['main', 'erf', 'superset'],
- };
- fetchMock.get(FETCH_SCHEMAS_GLOB, schemaOptions, {
+ describe('change schema', () => {
+ beforeEach(async () => {
+ fetchMock.get(FETCH_SCHEMAS_ENDPOINT, schemaOptions, {
overwriteRoutes: true,
});
+ });
+
+ afterEach(fetchMock.resetHistory);
+ afterAll(fetchMock.reset);
- return wrapper
- .instance()
- .fetchSchemas(1)
- .then(() => {
- expect(fetchMock.calls(FETCH_SCHEMAS_GLOB)).toHaveLength(1);
- expect(wrapper.state().schemaOptions).toHaveLength(3);
+ it('should fetch table', async () => {
+ fetchMock.get(GET_TABLE_NAMES_ENDPOINT, { overwriteRoutes: true });
+ act(() => {
+ wrapper.find('[data-test="select-database"]').first().props().onChange({
+ id: 1,
+ database_name: 'main',
});
+ });
+ await waitForComponentToPaint(wrapper);
+ act(() => {
+ wrapper
+ .find('[name="select-schema"]')
+ .first()
+ .props()
+ .onChange(selectedSchema);
+ });
+ await waitForComponentToPaint(wrapper);
+ expect(fetchMock.calls(GET_TABLE_NAMES_ENDPOINT)).toHaveLength(1);
});
- // Test needs to be fixed: Github issue #7768
- it.skip('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);
+ it('should fetch table options', async () => {
+ fetchMock.get(GET_TABLE_NAMES_ENDPOINT, tables, {
+ overwriteRoutes: true,
+ });
+ act(() => {
+ wrapper.find('[data-test="select-database"]').first().props().onChange({
+ id: 1,
+ database_name: 'main',
});
+ });
+ await waitForComponentToPaint(wrapper);
+ act(() => {
+ wrapper
+ .find('[name="select-schema"]')
+ .first()
+ .props()
+ .onChange(selectedSchema);
+ });
+ await waitForComponentToPaint(wrapper);
+ expect(
+ wrapper.find('[name="select-schema"]').first().props().value[0],
+ ).toEqual(selectedSchema);
+ expect(fetchMock.calls(GET_TABLE_NAMES_ENDPOINT)).toHaveLength(1);
+ const { options } = wrapper.find('[name="select-table"]').first().props();
+ expect({ options }).toEqual(tables);
});
});
- describe('changeTable', () => {
- beforeEach(() => {
- sinon.stub(wrapper.instance(), 'fetchTables');
- });
-
- afterEach(() => {
- wrapper.instance().fetchTables.restore();
+ describe('change table', () => {
+ beforeEach(async () => {
+ fetchMock.get(GET_TABLE_NAMES_ENDPOINT, tables, {
+ overwriteRoutes: true,
+ });
});
- it('test 1', () => {
- wrapper.instance().changeTable({
- value: 'birth_names',
- schema: 'main',
- label: 'birth_names',
- title: 'birth_names',
+ it('should change table value', async () => {
+ act(() => {
+ wrapper
+ .find('[name="select-schema"]')
+ .first()
+ .props()
+ .onChange(selectedSchema);
+ });
+ await waitForComponentToPaint(wrapper);
+ act(() => {
+ wrapper
+ .find('[name="select-table"]')
+ .first()
+ .props()
+ .onChange(selectedTable);
});
- expect(wrapper.state().tableName).toBe('birth_names');
+ await waitForComponentToPaint(wrapper);
+ expect(
+ wrapper.find('[name="select-table"]').first().props().value,
+ ).toEqual('birth_names');
});
- it('should call onTableChange with schema from table object', () => {
- wrapper.setProps({ schema: null });
- wrapper.instance().changeTable({
- value: 'my_table',
- schema: 'other_schema',
- label: 'other_schema.my_table',
- title: 'other_schema.my_table',
+ it('should call onTableChange with schema from table object', async () => {
+ act(() => {
+ wrapper
+ .find('[name="select-schema"]')
+ .first()
+ .props()
+ .onChange(selectedSchema);
});
- expect(mockedProps.onTableChange.getCall(0).args[0]).toBe('my_table');
- expect(mockedProps.onTableChange.getCall(0).args[1]).toBe('other_schema');
+ await waitForComponentToPaint(wrapper);
+ act(() => {
+ wrapper
+ .find('[name="select-table"]')
+ .first()
+ .props()
+ .onChange(selectedTable);
+ });
+ await waitForComponentToPaint(wrapper);
+ expect(mockedProps.onTableChange.getCall(0).args[0]).toBe('birth_names');
+ expect(mockedProps.onTableChange.getCall(0).args[1]).toBe('main');
});
});
- it('changeSchema', () => {
- sinon.stub(wrapper.instance(), 'fetchTables');
+ describe('getTableNamesBySubStr', () => {
+ afterEach(fetchMock.resetHistory);
+ afterAll(fetchMock.reset);
- 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);
+ it('should handle empty', async () => {
+ act(() => {
+ wrapper
+ .find('[name="async-select-table"]')
+ .first()
+ .props()
+ .loadOptions();
+ });
+ await waitForComponentToPaint(wrapper);
+ const props = wrapper.find('[name="async-select-table"]').first().props();
+ expect(props.isDisabled).toBe(true);
+ expect(props.value).toEqual('');
+ });
- wrapper.instance().fetchTables.restore();
+ it('should handle table name', async () => {
+ wrapper.setProps({ schema: 'main' });
+ fetchMock.get(GET_TABLE_ENDPOINT, tables, {
+ overwriteRoutes: true,
+ });
+ act(() => {
+ wrapper
+ .find('[name="async-select-table"]')
+ .first()
+ .props()
+ .loadOptions();
+ });
+ await waitForComponentToPaint(wrapper);
+ expect(fetchMock.calls(GET_TABLE_ENDPOINT)).toHaveLength(1);
+ });
});
});
diff --git a/superset-frontend/spec/javascripts/datasource/DatasourceModal_spec.jsx b/superset-frontend/spec/javascripts/datasource/DatasourceModal_spec.jsx
index dde67f9..aa5171f 100644
--- a/superset-frontend/spec/javascripts/datasource/DatasourceModal_spec.jsx
+++ b/superset-frontend/spec/javascripts/datasource/DatasourceModal_spec.jsx
@@ -60,12 +60,10 @@ async function mountAndWait(props = mockedProps) {
}
describe('DatasourceModal', () => {
- fetchMock.post(SAVE_ENDPOINT, SAVE_PAYLOAD);
- const callsP = fetchMock.put(SAVE_ENDPOINT, SAVE_PAYLOAD);
-
let wrapper;
beforeEach(async () => {
+ fetchMock.reset();
wrapper = await mountAndWait();
});
@@ -82,6 +80,7 @@ describe('DatasourceModal', () => {
});
it('saves on confirm', async () => {
+ const callsP = fetchMock.post(SAVE_ENDPOINT, SAVE_PAYLOAD);
act(() => {
wrapper
.find('button[data-test="datasource-modal-save"]')
@@ -94,6 +93,9 @@ describe('DatasourceModal', () => {
okButton.simulate('click');
});
await waitForComponentToPaint(wrapper);
- expect(callsP._calls).toHaveLength(2); /* eslint no-underscore-dangle: 0 */
+ const expected = ['http://localhost/datasource/save/'];
+ expect(callsP._calls.map(call => call[0])).toEqual(
+ expected,
+ ); /* eslint no-underscore-dangle: 0 */
});
});
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx
index 552ba4c..66539b2 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx
@@ -120,17 +120,18 @@ export default class SqlEditorLeftBar extends React.PureComponent {
return (
<div className="SqlEditorLeftBar">
<TableSelector
+ database={this.props.database}
dbId={qe.dbId}
- schema={qe.schema}
+ getDbList={this.getDbList}
+ handleError={this.props.actions.addDangerToast}
onDbChange={this.onDbChange}
onSchemaChange={this.onSchemaChange}
onSchemasLoad={this.onSchemasLoad}
- onTablesLoad={this.onTablesLoad}
- getDbList={this.getDbList}
onTableChange={this.onTableChange}
+ onTablesLoad={this.onTablesLoad}
+ schema={qe.schema}
+ sqlLabMode
tableNameSticky={false}
- database={this.props.database}
- handleError={this.props.actions.addDangerToast}
/>
<div className="divider" />
<div className="scrollbar-container">
diff --git a/superset-frontend/src/components/DatabaseSelector.tsx b/superset-frontend/src/components/DatabaseSelector.tsx
new file mode 100644
index 0000000..f426e1a
--- /dev/null
+++ b/superset-frontend/src/components/DatabaseSelector.tsx
@@ -0,0 +1,283 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React, { ReactNode, useEffect, useState } from 'react';
+import { styled, SupersetClient, t } from '@superset-ui/core';
+import rison from 'rison';
+import { Select } from 'src/components/Select';
+import Label from 'src/components/Label';
+
+import SupersetAsyncSelect from './AsyncSelect';
+import RefreshLabel from './RefreshLabel';
+
+const FieldTitle = styled.p`
+ color: ${({ theme }) => theme.colors.secondary.light2};
+ font-size: ${({ theme }) => theme.typography.sizes.s}px;
+ margin: 20px 0 10px 0;
+ text-transform: uppercase;
+`;
+
+const DatabaseSelectorWrapper = styled.div`
+ .fa-refresh {
+ padding-left: 9px;
+ }
+
+ .refresh-col {
+ display: flex;
+ align-items: center;
+ width: 30px;
+ }
+
+ .section {
+ padding-bottom: 5px;
+ display: flex;
+ flex-direction: row;
+ }
+
+ .select {
+ flex-grow: 1;
+ }
+`;
+
+interface DatabaseSelectorProps {
+ dbId: number;
+ formMode?: boolean;
+ getDbList?: (arg0: any) => {};
+ getTableList?: (dbId: number, schema: string, force: boolean) => {};
+ handleError: (msg: string) => void;
+ isDatabaseSelectEnabled?: boolean;
+ onDbChange?: (db: any) => void;
+ onSchemaChange?: (arg0?: any) => {};
+ onSchemasLoad?: (schemas: Array<object>) => void;
+ schema?: string;
+ sqlLabMode?: boolean;
+ onChange?: ({
+ dbId,
+ schema,
+ }: {
+ dbId: number;
+ schema?: string;
+ tableName?: string;
+ }) => void;
+}
+
+export default function DatabaseSelector({
+ dbId,
+ formMode = false,
+ getDbList,
+ getTableList,
+ handleError,
+ isDatabaseSelectEnabled = true,
+ onChange,
+ onDbChange,
+ onSchemaChange,
+ onSchemasLoad,
+ schema,
+ sqlLabMode = false,
+}: DatabaseSelectorProps) {
+ const [currentDbId, setCurrentDbId] = useState(dbId);
+ const [currentSchema, setCurrentSchema] = useState<string | undefined>(
+ schema,
+ );
+ const [schemaLoading, setSchemaLoading] = useState(false);
+ const [schemaOptions, setSchemaOptions] = useState([]);
+
+ function fetchSchemas(databaseId: number, forceRefresh = false) {
+ const actualDbId = databaseId || dbId;
+ if (actualDbId) {
+ setSchemaLoading(true);
+ const queryParams = rison.encode({
+ force: Boolean(forceRefresh),
+ });
+ const endpoint = `/api/v1/database/${actualDbId}/schemas/?q=${queryParams}`;
+ return SupersetClient.get({ endpoint })
+ .then(({ json }) => {
+ const options = json.result.map((s: string) => ({
+ value: s,
+ label: s,
+ title: s,
+ }));
+ setSchemaOptions(options);
+ setSchemaLoading(false);
+ if (onSchemasLoad) {
+ onSchemasLoad(options);
+ }
+ })
+ .catch(() => {
+ setSchemaOptions([]);
+ setSchemaLoading(false);
+ handleError(t('Error while fetching schema list'));
+ });
+ }
+ return Promise.resolve();
+ }
+
+ useEffect(() => {
+ if (currentDbId) {
+ fetchSchemas(currentDbId);
+ }
+ }, [currentDbId]);
+
+ function onSelectChange({ dbId, schema }: { dbId: number; schema?: string }) {
+ setCurrentDbId(dbId);
+ setCurrentSchema(schema);
+ if (onChange) {
+ onChange({ dbId, schema, tableName: undefined });
+ }
+ }
+
+ function dbMutator(data: any) {
+ if (getDbList) {
+ getDbList(data.result);
+ }
+ if (data.result.length === 0) {
+ handleError(t("It seems you don't have access to any database"));
+ }
+ return data.result.map((row: any) => ({
+ ...row,
+ // label is used for the typeahead
+ label: `${row.backend} ${row.database_name}`,
+ }));
+ }
+
+ function changeDataBase(db: any, force = false) {
+ const dbId = db ? db.id : null;
+ setSchemaOptions([]);
+ if (onSchemaChange) {
+ onSchemaChange(null);
+ }
+ if (onDbChange) {
+ onDbChange(db);
+ }
+ fetchSchemas(dbId, force);
+ onSelectChange({ dbId, schema: undefined });
+ }
+
+ function changeSchema(schemaOpt: any, force = false) {
+ const schema = schemaOpt ? schemaOpt.value : null;
+ if (onSchemaChange) {
+ onSchemaChange(schema);
+ }
+ setCurrentSchema(schema);
+ onSelectChange({ dbId: currentDbId, schema });
+ if (getTableList) {
+ getTableList(currentDbId, schema, force);
+ }
+ }
+
+ function renderDatabaseOption(db: any) {
+ return (
+ <span title={db.database_name}>
+ <Label bsStyle="default">{db.backend}</Label>
+ {db.database_name}
+ </span>
+ );
+ }
+
+ function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
+ return (
+ <div className="section">
+ <span className="select">{select}</span>
+ <span className="refresh-col">{refreshBtn}</span>
+ </div>
+ );
+ }
+
+ function renderDatabaseSelect() {
+ const queryParams = rison.encode({
+ order_columns: 'database_name',
+ order_direction: 'asc',
+ page: 0,
+ page_size: -1,
+ ...(formMode || !sqlLabMode
+ ? {}
+ : {
+ filters: [
+ {
+ col: 'expose_in_sqllab',
+ opr: 'eq',
+ value: true,
+ },
+ ],
+ }),
+ });
+
+ return renderSelectRow(
+ <SupersetAsyncSelect
+ data-test="select-database"
+ dataEndpoint={`/api/v1/database/?q=${queryParams}`}
+ onChange={(db: any) => changeDataBase(db)}
+ onAsyncError={() =>
+ handleError(t('Error while fetching database list'))
+ }
+ clearable={false}
+ value={currentDbId}
+ valueKey="id"
+ valueRenderer={(db: any) => (
+ <div>
+ <span className="text-muted m-r-5">{t('Database:')}</span>
+ {renderDatabaseOption(db)}
+ </div>
+ )}
+ optionRenderer={renderDatabaseOption}
+ mutator={dbMutator}
+ placeholder={t('Select a database')}
+ autoSelect
+ isDisabled={!isDatabaseSelectEnabled}
+ />,
+ null,
+ );
+ }
+
+ function renderSchemaSelect() {
+ const value = schemaOptions.filter(({ value }) => currentSchema === value);
+ const refresh = !formMode && (
+ <RefreshLabel
+ onClick={() => changeDataBase({ id: dbId }, true)}
+ tooltipContent={t('Force refresh schema list')}
+ />
+ );
+
+ return renderSelectRow(
+ <Select
+ name="select-schema"
+ placeholder={t('Select a schema (%s)', schemaOptions.length)}
+ options={schemaOptions}
+ value={value}
+ valueRenderer={o => (
+ <div>
+ <span className="text-muted">{t('Schema:')}</span> {o.label}
+ </div>
+ )}
+ isLoading={schemaLoading}
+ autosize={false}
+ onChange={item => changeSchema(item)}
+ />,
+ refresh,
+ );
+ }
+
+ return (
+ <DatabaseSelectorWrapper>
+ {formMode && <FieldTitle>{t('datasource')}</FieldTitle>}
+ {renderDatabaseSelect()}
+ {formMode && <FieldTitle>{t('schema')}</FieldTitle>}
+ {renderSchemaSelect()}
+ </DatabaseSelectorWrapper>
+ );
+}
diff --git a/superset-frontend/src/components/TableSelector.jsx b/superset-frontend/src/components/TableSelector.jsx
deleted file mode 100644
index 395c6be..0000000
--- a/superset-frontend/src/components/TableSelector.jsx
+++ /dev/null
@@ -1,446 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React from 'react';
-import { styled, SupersetClient, t } from '@superset-ui/core';
-import PropTypes from 'prop-types';
-import rison from 'rison';
-import { AsyncSelect, CreatableSelect, Select } from 'src/components/Select';
-
-import Label from 'src/components/Label';
-import FormLabel from 'src/components/FormLabel';
-
-import SupersetAsyncSelect from './AsyncSelect';
-import RefreshLabel from './RefreshLabel';
-import './TableSelector.less';
-
-const FieldTitle = styled.p`
- color: ${({ theme }) => theme.colors.secondary.light2};
- font-size: ${({ theme }) => theme.typography.sizes.s}px;
- margin: 20px 0 10px 0;
- text-transform: uppercase;
-`;
-
-const propTypes = {
- dbId: PropTypes.number.isRequired,
- schema: PropTypes.string,
- onSchemaChange: PropTypes.func,
- onDbChange: PropTypes.func,
- onSchemasLoad: PropTypes.func,
- onTablesLoad: PropTypes.func,
- getDbList: PropTypes.func,
- onTableChange: PropTypes.func,
- tableNameSticky: PropTypes.bool,
- tableName: PropTypes.string,
- database: PropTypes.object,
- sqlLabMode: PropTypes.bool,
- formMode: PropTypes.bool,
- onChange: PropTypes.func,
- clearable: PropTypes.bool,
- handleError: PropTypes.func.isRequired,
- isDatabaseSelectEnabled: PropTypes.bool,
-};
-
-const defaultProps = {
- onDbChange: () => {},
- onSchemaChange: () => {},
- onSchemasLoad: () => {},
- onTablesLoad: () => {},
- getDbList: () => {},
- onTableChange: () => {},
- onChange: () => {},
- tableNameSticky: true,
- sqlLabMode: true,
- formMode: false,
- clearable: true,
- isDatabaseSelectEnabled: 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,
- };
- this.onDatabaseChange = this.onDatabaseChange.bind(this);
- this.onSchemaChange = this.onSchemaChange.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);
- }
-
- componentDidMount() {
- if (this.state.dbId) {
- this.fetchSchemas(this.state.dbId);
- this.fetchTables();
- }
- }
-
- onChange() {
- this.props.onChange({
- dbId: this.state.dbId,
- schema: this.state.schema,
- tableName: this.state.tableName,
- });
- }
-
- onDatabaseChange(db, force) {
- return this.changeDataBase(db, force);
- }
-
- onSchemaChange(schemaOpt) {
- return this.changeSchema(schemaOpt);
- }
-
- getTableNamesBySubStr(substr = 'undefined') {
- if (!this.props.dbId || !substr) {
- const options = [];
- return Promise.resolve({ options });
- }
- const encodedSchema = encodeURIComponent(this.props.schema);
- const encodedSubstr = encodeURIComponent(substr);
- return SupersetClient.get({
- endpoint: encodeURI(
- `/superset/tables/${this.props.dbId}/${encodedSchema}/${encodedSubstr}`,
- ),
- }).then(({ json }) => {
- const options = json.options.map(o => ({
- value: o.value,
- schema: o.schema,
- label: o.label,
- title: o.title,
- type: o.type,
- }));
- return { options };
- });
- }
-
- 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.map(row => ({
- ...row,
- // label is used for the typeahead
- label: `${row.backend} ${row.database_name}`,
- }));
- }
-
- fetchTables(forceRefresh = false, substr = 'undefined') {
- const { dbId, schema } = this.state;
- const encodedSchema = encodeURIComponent(schema);
- const encodedSubstr = encodeURIComponent(substr);
- if (dbId && schema) {
- this.setState(() => ({ tableLoading: true, tableOptions: [] }));
- const endpoint = encodeURI(
- `/superset/tables/${dbId}/${encodedSchema}/${encodedSubstr}/${!!forceRefresh}/`,
- );
- return SupersetClient.get({ endpoint })
- .then(({ json }) => {
- const options = json.options.map(o => ({
- value: o.value,
- schema: o.schema,
- label: o.label,
- title: o.title,
- type: o.type,
- }));
- this.setState(() => ({
- tableLoading: false,
- tableOptions: options,
- }));
- this.props.onTablesLoad(json.options);
- })
- .catch(() => {
- this.setState(() => ({ tableLoading: false, tableOptions: [] }));
- this.props.handleError(t('Error while fetching table list'));
- });
- }
- this.setState(() => ({ tableLoading: false, tableOptions: [] }));
- return Promise.resolve();
- }
-
- fetchSchemas(dbId, forceRefresh = false) {
- const actualDbId = dbId || this.props.dbId;
- if (actualDbId) {
- this.setState({ schemaLoading: true });
- const queryParams = rison.encode({
- force: Boolean(forceRefresh),
- });
- const endpoint = `/api/v1/database/${actualDbId}/schemas/?q=${queryParams}`;
- return SupersetClient.get({ endpoint })
- .then(({ json }) => {
- const schemaOptions = json.result.map(s => ({
- value: s,
- label: s,
- title: s,
- }));
- this.setState({ schemaOptions, schemaLoading: false });
- this.props.onSchemasLoad(schemaOptions);
- })
- .catch(() => {
- this.setState({ schemaLoading: false, schemaOptions: [] });
- this.props.handleError(t('Error while fetching schema list'));
- });
- }
- return Promise.resolve();
- }
-
- changeDataBase(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, tableName: null, tableOptions: [] },
- this.onChange,
- );
- }
-
- changeSchema(schemaOpt, force = false) {
- const schema = schemaOpt ? schemaOpt.value : null;
- this.props.onSchemaChange(schema);
- this.setState({ schema }, () => {
- this.fetchTables(force);
- this.onChange();
- });
- }
-
- changeTable(tableOpt) {
- if (!tableOpt) {
- this.setState({ tableName: '' });
- return;
- }
- const schemaName = tableOpt.schema;
- const tableName = tableOpt.value;
- if (this.props.tableNameSticky) {
- this.setState({ tableName }, this.onChange);
- }
- this.props.onTableChange(tableName, schemaName);
- }
-
- renderDatabaseOption(db) {
- return (
- <span title={db.database_name}>
- <Label bsStyle="default">{db.backend}</Label>
- {db.database_name}
- </span>
- );
- }
-
- renderTableOption(option) {
- return (
- <span className="TableLabel" title={option.label}>
- <span className="m-r-5">
- <small className="text-muted">
- <i
- className={`fa fa-${option.type === 'view' ? 'eye' : 'table'}`}
- />
- </small>
- </span>
- {option.label}
- </span>
- );
- }
-
- renderSelectRow(select, refreshBtn) {
- return (
- <div className="section">
- <span className="select">{select}</span>
- <span className="refresh-col">{refreshBtn}</span>
- </div>
- );
- }
-
- renderDatabaseSelect() {
- const queryParams = rison.encode({
- order_columns: 'database_name',
- order_direction: 'asc',
- page: 0,
- page_size: -1,
- ...(this.props.formMode
- ? {}
- : {
- filters: [
- {
- col: 'expose_in_sqllab',
- opr: 'eq',
- value: true,
- },
- ],
- }),
- });
-
- return this.renderSelectRow(
- <SupersetAsyncSelect
- dataEndpoint={`/api/v1/database/?q=${queryParams}`}
- 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')}
- isDisabled={!this.props.isDatabaseSelectEnabled}
- autoSelect
- />,
- );
- }
-
- renderSchema() {
- const refresh = !this.props.formMode && (
- <RefreshLabel
- onClick={() => this.onDatabaseChange({ id: this.props.dbId }, true)}
- tooltipContent={t('Force refresh schema list')}
- />
- );
- return this.renderSelectRow(
- <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.onSchemaChange}
- />,
- refresh,
- );
- }
-
- 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.state.tableOptions;
- let select = null;
- if (this.props.schema && !this.props.formMode) {
- select = (
- <Select
- name="select-table"
- isLoading={this.state.tableLoading}
- ignoreAccents={false}
- placeholder={t('Select table or type table name')}
- autosize={false}
- onChange={this.changeTable}
- options={options}
- value={this.state.tableName}
- optionRenderer={this.renderTableOption}
- />
- );
- } else if (this.props.formMode) {
- select = (
- <CreatableSelect
- name="select-table"
- isLoading={this.state.tableLoading}
- ignoreAccents={false}
- placeholder={t('Select table or type table name')}
- autosize={false}
- onChange={this.changeTable}
- options={options}
- value={this.state.tableName}
- optionRenderer={this.renderTableOption}
- />
- );
- } else {
- select = (
- <AsyncSelect
- name="async-select-table"
- placeholder={tableSelectPlaceholder}
- isDisabled={tableSelectDisabled}
- autosize={false}
- onChange={this.changeTable}
- value={this.state.tableName}
- loadOptions={this.getTableNamesBySubStr}
- optionRenderer={this.renderTableOption}
- />
- );
- }
- const refresh = !this.props.formMode && (
- <RefreshLabel
- onClick={() => this.changeSchema({ value: this.props.schema }, true)}
- tooltipContent={t('Force refresh table list')}
- />
- );
- return this.renderSelectRow(select, refresh);
- }
-
- renderSeeTableLabel() {
- return (
- <div className="section">
- <FormLabel>
- {t('See table schema')}{' '}
- {this.props.schema && (
- <small>
- ({this.state.tableOptions.length} in <i>{this.props.schema}</i>)
- </small>
- )}
- </FormLabel>
- </div>
- );
- }
-
- render() {
- return (
- <div className="TableSelector">
- {this.props.formMode && <FieldTitle>{t('datasource')}</FieldTitle>}
- {this.renderDatabaseSelect()}
- {this.props.formMode && <FieldTitle>{t('schema')}</FieldTitle>}
- {this.renderSchema()}
- {!this.props.formMode && <div className="divider" />}
- {this.props.sqlLabMode && this.renderSeeTableLabel()}
- {this.props.formMode && <FieldTitle>{t('Table')}</FieldTitle>}
- {this.renderTable()}
- </div>
- );
- }
-}
-TableSelector.propTypes = propTypes;
-TableSelector.defaultProps = defaultProps;
diff --git a/superset-frontend/src/components/TableSelector.tsx b/superset-frontend/src/components/TableSelector.tsx
new file mode 100644
index 0000000..4c59359
--- /dev/null
+++ b/superset-frontend/src/components/TableSelector.tsx
@@ -0,0 +1,379 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React, {
+ FunctionComponent,
+ useEffect,
+ useState,
+ ReactNode,
+} from 'react';
+import { styled, SupersetClient, t } from '@superset-ui/core';
+import { AsyncSelect, CreatableSelect, Select } from 'src/components/Select';
+
+import FormLabel from 'src/components/FormLabel';
+
+import DatabaseSelector from './DatabaseSelector';
+import RefreshLabel from './RefreshLabel';
+
+const FieldTitle = styled.p`
+ color: ${({ theme }) => theme.colors.secondary.light2};
+ font-size: ${({ theme }) => theme.typography.sizes.s}px;
+ margin: 20px 0 10px 0;
+ text-transform: uppercase;
+`;
+
+const TableSelectorWrapper = styled.div`
+ .fa-refresh {
+ padding-left: 9px;
+ }
+
+ .refresh-col {
+ display: flex;
+ align-items: center;
+ width: 30px;
+ }
+
+ .section {
+ padding-bottom: 5px;
+ display: flex;
+ flex-direction: row;
+ }
+
+ .select {
+ flex-grow: 1;
+ }
+
+ .divider {
+ border-bottom: 1px solid ${({ theme }) => theme.colors.secondary.light5};
+ margin: 15px 0;
+ }
+`;
+
+const TableLabel = styled.span`
+ white-space: nowrap;
+`;
+
+interface TableSelectorProps {
+ clearable?: boolean;
+ database?: any;
+ dbId: number;
+ formMode?: boolean;
+ getDbList?: (arg0: any) => {};
+ handleError: (msg: string) => void;
+ isDatabaseSelectEnabled?: boolean;
+ onChange?: ({
+ dbId,
+ schema,
+ }: {
+ dbId: number;
+ schema?: string;
+ tableName?: string;
+ }) => void;
+ onDbChange?: (db: any) => void;
+ onSchemaChange?: (arg0?: any) => {};
+ onSchemasLoad?: () => void;
+ onTableChange?: (tableName: string, schema: string) => void;
+ onTablesLoad?: (options: Array<any>) => {};
+ schema?: string;
+ sqlLabMode?: boolean;
+ tableName?: string;
+ tableNameSticky?: boolean;
+}
+
+const TableSelector: FunctionComponent<TableSelectorProps> = ({
+ database,
+ dbId,
+ formMode = false,
+ getDbList,
+ handleError,
+ isDatabaseSelectEnabled = true,
+ onChange,
+ onDbChange,
+ onSchemaChange,
+ onSchemasLoad,
+ onTableChange,
+ onTablesLoad,
+ schema,
+ sqlLabMode = true,
+ tableName,
+ tableNameSticky = true,
+}) => {
+ const [currentSchema, setCurrentSchema] = useState<string | undefined>(
+ schema,
+ );
+ const [currentTableName, setCurrentTableName] = useState<string | undefined>(
+ tableName,
+ );
+ const [tableLoading, setTableLoading] = useState(false);
+ const [tableOptions, setTableOptions] = useState([]);
+
+ function fetchTables(
+ databaseId?: number,
+ schema?: string,
+ forceRefresh = false,
+ substr = 'undefined',
+ ) {
+ const dbSchema = schema || currentSchema;
+ const actualDbId = databaseId || dbId;
+ if (actualDbId && dbSchema) {
+ const encodedSchema = encodeURIComponent(dbSchema);
+ const encodedSubstr = encodeURIComponent(substr);
+ setTableLoading(true);
+ setTableOptions([]);
+ const endpoint = encodeURI(
+ `/superset/tables/${actualDbId}/${encodedSchema}/${encodedSubstr}/${!!forceRefresh}/`,
+ );
+ return SupersetClient.get({ endpoint })
+ .then(({ json }) => {
+ const options = json.options.map((o: any) => ({
+ value: o.value,
+ schema: o.schema,
+ label: o.label,
+ title: o.title,
+ type: o.type,
+ }));
+ setTableLoading(false);
+ setTableOptions(options);
+ if (onTablesLoad) {
+ onTablesLoad(json.options);
+ }
+ })
+ .catch(() => {
+ setTableLoading(false);
+ setTableOptions([]);
+ handleError(t('Error while fetching table list'));
+ });
+ }
+ setTableLoading(false);
+ setTableOptions([]);
+ return Promise.resolve();
+ }
+
+ useEffect(() => {
+ if (dbId && schema) {
+ fetchTables();
+ }
+ }, [dbId, schema]);
+
+ function onSelectionChange({
+ dbId,
+ schema,
+ tableName,
+ }: {
+ dbId: number;
+ schema?: string;
+ tableName?: string;
+ }) {
+ setCurrentTableName(tableName);
+ setCurrentSchema(schema);
+ if (onChange) {
+ onChange({ dbId, schema, tableName });
+ }
+ }
+
+ function getTableNamesBySubStr(substr = 'undefined') {
+ if (!dbId || !substr) {
+ const options: any[] = [];
+ return Promise.resolve({ options });
+ }
+ const encodedSchema = encodeURIComponent(schema || '');
+ const encodedSubstr = encodeURIComponent(substr);
+ return SupersetClient.get({
+ endpoint: encodeURI(
+ `/superset/tables/${dbId}/${encodedSchema}/${encodedSubstr}`,
+ ),
+ }).then(({ json }) => {
+ const options = json.options.map((o: any) => ({
+ value: o.value,
+ schema: o.schema,
+ label: o.label,
+ title: o.title,
+ type: o.type,
+ }));
+ return { options };
+ });
+ }
+
+ function changeTable(tableOpt: any) {
+ if (!tableOpt) {
+ setCurrentTableName('');
+ return;
+ }
+ const schemaName = tableOpt.schema;
+ const tableOptTableName = tableOpt.value;
+ if (tableNameSticky) {
+ onSelectionChange({
+ dbId,
+ schema: schemaName,
+ tableName: tableOptTableName,
+ });
+ }
+ if (onTableChange) {
+ onTableChange(tableOptTableName, schemaName);
+ }
+ }
+
+ function changeSchema(schemaOpt: any, force = false) {
+ const value = schemaOpt ? schemaOpt.value : null;
+ if (onSchemaChange) {
+ onSchemaChange(value);
+ }
+ onSelectionChange({
+ dbId,
+ schema: value,
+ tableName: undefined,
+ });
+ fetchTables(dbId, currentSchema, force);
+ }
+
+ function renderTableOption(option: any) {
+ return (
+ <TableLabel title={option.label}>
+ <span className="m-r-5">
+ <small className="text-muted">
+ <i
+ className={`fa fa-${option.type === 'view' ? 'eye' : 'table'}`}
+ />
+ </small>
+ </span>
+ {option.label}
+ </TableLabel>
+ );
+ }
+
+ function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
+ return (
+ <div className="section">
+ <span className="select">{select}</span>
+ <span className="refresh-col">{refreshBtn}</span>
+ </div>
+ );
+ }
+
+ function renderDatabaseSelector() {
+ return (
+ <DatabaseSelector
+ dbId={dbId}
+ formMode={formMode}
+ getDbList={getDbList}
+ getTableList={fetchTables}
+ handleError={handleError}
+ onChange={onSelectionChange}
+ onDbChange={onDbChange}
+ onSchemaChange={onSchemaChange}
+ onSchemasLoad={onSchemasLoad}
+ schema={currentSchema}
+ sqlLabMode={sqlLabMode}
+ isDatabaseSelectEnabled={isDatabaseSelectEnabled}
+ />
+ );
+ }
+
+ function renderTableSelect() {
+ let tableSelectPlaceholder;
+ let tableSelectDisabled = false;
+ if (database && database.allow_multi_schema_metadata_fetch) {
+ tableSelectPlaceholder = t('Type to search ...');
+ } else {
+ tableSelectPlaceholder = t('Select table ');
+ tableSelectDisabled = true;
+ }
+ const options = tableOptions;
+ let select = null;
+ if (currentSchema && !formMode) {
+ select = (
+ <Select
+ name="select-table"
+ isLoading={tableLoading}
+ ignoreAccents={false}
+ placeholder={t('Select table or type table name')}
+ autosize={false}
+ onChange={changeTable}
+ options={options}
+ // @ts-ignore
+ value={currentTableName}
+ optionRenderer={renderTableOption}
+ />
+ );
+ } else if (formMode) {
+ select = (
+ <CreatableSelect
+ name="select-table"
+ isLoading={tableLoading}
+ ignoreAccents={false}
+ placeholder={t('Select table or type table name')}
+ autosize={false}
+ onChange={changeTable}
+ options={options}
+ // @ts-ignore
+ value={currentTableName}
+ optionRenderer={renderTableOption}
+ />
+ );
+ } else {
+ select = (
+ <AsyncSelect
+ name="async-select-table"
+ placeholder={tableSelectPlaceholder}
+ isDisabled={tableSelectDisabled}
+ autosize={false}
+ onChange={changeTable}
+ // @ts-ignore
+ value={currentTableName}
+ loadOptions={getTableNamesBySubStr}
+ optionRenderer={renderTableOption}
+ />
+ );
+ }
+ const refresh = !formMode && (
+ <RefreshLabel
+ onClick={() => changeSchema({ value: schema }, true)}
+ tooltipContent={t('Force refresh table list')}
+ />
+ );
+ return renderSelectRow(select, refresh);
+ }
+
+ function renderSeeTableLabel() {
+ return (
+ <div className="section">
+ <FormLabel>
+ {t('See table schema')}{' '}
+ {schema && (
+ <small>
+ {tableOptions.length} in
+ <i>{schema}</i>
+ </small>
+ )}
+ </FormLabel>
+ </div>
+ );
+ }
+
+ return (
+ <TableSelectorWrapper>
+ {renderDatabaseSelector()}
+ {!formMode && <div className="divider" />}
+ {sqlLabMode && renderSeeTableLabel()}
+ {formMode && <FieldTitle>{t('Table')}</FieldTitle>}
+ {renderTableSelect()}
+ </TableSelectorWrapper>
+ );
+};
+
+export default TableSelector;
diff --git a/superset-frontend/src/datasource/DatasourceEditor.jsx b/superset-frontend/src/datasource/DatasourceEditor.jsx
index d36bfae..c35a364 100644
--- a/superset-frontend/src/datasource/DatasourceEditor.jsx
+++ b/superset-frontend/src/datasource/DatasourceEditor.jsx
@@ -18,15 +18,16 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
-import { Alert, Badge, Col, Tabs, Tab, Well } from 'react-bootstrap';
+import { Alert, Badge, Col, Radio, Tabs, Tab, Well } from 'react-bootstrap';
import shortid from 'shortid';
import { styled, SupersetClient, t } from '@superset-ui/core';
-import Label from 'src/components/Label';
import Button from 'src/components/Button';
+import CertifiedIconWithTooltip from 'src/components/CertifiedIconWithTooltip';
+import DatabaseSelector from 'src/components/DatabaseSelector';
+import Label from 'src/components/Label';
import Loading from 'src/components/Loading';
import TableSelector from 'src/components/TableSelector';
-import CertifiedIconWithTooltip from 'src/components/CertifiedIconWithTooltip';
import EditableTitle from 'src/components/EditableTitle';
import getClientErrorObject from 'src/utils/getClientErrorObject';
@@ -74,6 +75,15 @@ const checkboxGenerator = (d, onChange) => (
);
const DATA_TYPES = ['STRING', 'NUMERIC', 'DATETIME'];
+const DATASOURCE_TYPES_ARR = [
+ { key: 'physical', label: t('Physical (table or view)') },
+ { key: 'virtual', label: t('Virtual (SQL)') },
+];
+const DATASOURCE_TYPES = {};
+DATASOURCE_TYPES_ARR.forEach(o => {
+ DATASOURCE_TYPES[o.key] = o;
+});
+
function CollectionTabTitle({ title, collection }) {
return (
<div>
@@ -278,7 +288,10 @@ class DatasourceEditor extends React.PureComponent {
col => !!col.expression,
),
metadataLoading: false,
- activeTabKey: 1,
+ activeTabKey: 0,
+ datasourceType: props.datasource.sql
+ ? DATASOURCE_TYPES.virtual.key
+ : DATASOURCE_TYPES.physical.key,
};
this.onChange = this.onChange.bind(this);
@@ -291,11 +304,19 @@ class DatasourceEditor extends React.PureComponent {
}
onChange() {
- const datasource = {
+ // Emptying SQL if "Physical" radio button is selected
+ // Currently the logic to know whether the source is
+ // physical or virtual is based on whether SQL is empty or not.
+ const { datasourceType, datasource } = this.state;
+ const sql =
+ datasourceType === DATASOURCE_TYPES.physical.key ? '' : datasource.sql;
+
+ const newDatasource = {
...this.state.datasource,
+ sql,
columns: [...this.state.databaseColumns, ...this.state.calculatedColumns],
};
- this.props.onChange(datasource, this.state.errors);
+ this.props.onChange(newDatasource, this.state.errors);
}
onDatasourceChange(datasource) {
@@ -310,6 +331,10 @@ class DatasourceEditor extends React.PureComponent {
);
}
+ onDatasourceTypeChange(datasourceType) {
+ this.setState({ datasourceType });
+ }
+
setColumns(obj) {
this.setState(obj, this.validateAndChange);
}
@@ -466,34 +491,6 @@ class DatasourceEditor extends React.PureComponent {
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)
- }
- onTableChange={table =>
- this.onDatasourcePropChange('datasource_name', table)
- }
- sqlLabMode={false}
- clearable={false}
- handleError={this.props.addDangerToast}
- isDatabaseSelectEnabled={false}
- />
- }
- description={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')}
@@ -559,32 +556,6 @@ class DatasourceEditor extends React.PureComponent {
item={datasource}
onChange={this.onDatasourceChange}
>
- {this.state.isSqla && (
- <Field
- fieldKey="sql"
- label={t('SQL')}
- description={t(
- 'When specifying SQL, the dataset acts as a view. ' +
- 'Superset will use this statement as a subquery while grouping and filtering ' +
- 'on the generated parent queries.',
- )}
- control={
- <TextAreaControl language="sql" offerEditInModal={false} />
- }
- />
- )}
- {this.state.isDruid && (
- <Field
- fieldKey="json"
- label={t('JSON')}
- description={
- <div>{t('The JSON metric or post aggregation definition.')}</div>
- }
- control={
- <TextAreaControl language="json" offerEditInModal={false} />
- }
- />
- )}
<Field
fieldKey="cache_timeout"
label={t('Cache Timeout')}
@@ -645,6 +616,121 @@ class DatasourceEditor extends React.PureComponent {
);
}
+ renderSourceFieldset() {
+ const { datasource } = this.state;
+ return (
+ <div>
+ <div className="m-l-10 m-t-20 m-b-10">
+ {DATASOURCE_TYPES_ARR.map(type => (
+ <Radio
+ value={type.key}
+ inline
+ onChange={this.onDatasourceTypeChange.bind(this, type.key)}
+ checked={this.state.datasourceType === type.key}
+ >
+ {type.label}
+ </Radio>
+ ))}
+ </div>
+ <hr />
+ <Fieldset item={datasource} onChange={this.onDatasourceChange} compact>
+ {this.state.datasourceType === DATASOURCE_TYPES.virtual.key && (
+ <div>
+ {this.state.isSqla && (
+ <>
+ <Field
+ fieldKey="databaseSelector"
+ label={t('virtual')}
+ control={
+ <DatabaseSelector
+ dbId={datasource.database.id}
+ schema={datasource.schema}
+ onSchemaChange={schema =>
+ this.onDatasourcePropChange('schema', schema)
+ }
+ onDbChange={database =>
+ this.onDatasourcePropChange('database', database)
+ }
+ formMode={false}
+ handleError={this.props.addDangerToast}
+ />
+ }
+ />
+ <Field
+ fieldKey="sql"
+ label={t('SQL')}
+ description={t(
+ 'When specifying SQL, the datasource acts as a view. ' +
+ 'Superset will use this statement as a subquery while grouping and filtering ' +
+ 'on the generated parent queries.',
+ )}
+ control={
+ <TextAreaControl
+ language="sql"
+ offerEditInModal={false}
+ minLines={25}
+ maxLines={25}
+ />
+ }
+ />
+ </>
+ )}
+ {this.state.isDruid && (
+ <Field
+ fieldKey="json"
+ label={t('JSON')}
+ description={
+ <div>
+ {t('The JSON metric or post aggregation definition.')}
+ </div>
+ }
+ control={
+ <TextAreaControl language="json" offerEditInModal={false} />
+ }
+ />
+ )}
+ </div>
+ )}
+ {this.state.datasourceType === DATASOURCE_TYPES.physical.key && (
+ <Col md={6}>
+ {this.state.isSqla && (
+ <Field
+ fieldKey="tableSelector"
+ label={t('Physical')}
+ control={
+ <TableSelector
+ clearable={false}
+ dbId={datasource.database.id}
+ handleError={this.props.addDangerToast}
+ schema={datasource.schema}
+ sqlLabMode={false}
+ tableName={datasource.table_name}
+ onSchemaChange={schema =>
+ this.onDatasourcePropChange('schema', schema)
+ }
+ onDbChange={database =>
+ this.onDatasourcePropChange('database', database)
+ }
+ onTableChange={table => {
+ this.onDatasourcePropChange('table_name', table);
+ }}
+ isDatabaseSelectEnabled={false}
+ />
+ }
+ description={t(
+ 'The pointer to a physical table (or view). Keep in mind that the chart is ' +
+ 'associated to this Superset logical table, and this logical table points ' +
+ 'the physical table referenced here.',
+ )}
+ />
+ )}
+ </Col>
+ )}
+ </Fieldset>
+ </div>
+ );
+ }
+
renderErrors() {
if (this.state.errors.length > 0) {
return (
@@ -800,6 +886,9 @@ class DatasourceEditor extends React.PureComponent {
onSelect={this.handleTabSelect}
defaultActiveKey={activeTabKey}
>
+ <Tab eventKey={0} title={t('Source')}>
+ {activeTabKey === 0 && this.renderSourceFieldset()}
+ </Tab>
<Tab
title={
<CollectionTabTitle
diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx
index 9edd4db..2acf238 100644
--- a/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx
+++ b/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx
@@ -44,10 +44,8 @@ const StyledIcon = styled(Icon)`
`;
const TableSelectorContainer = styled.div`
- .TableSelector {
- padding-bottom: 340px;
- width: 65%;
- }
+ padding-bottom: 340px;
+ width: 65%;
`;
const DatasetModal: FunctionComponent<DatasetModalProps> = ({
@@ -59,7 +57,7 @@ const DatasetModal: FunctionComponent<DatasetModalProps> = ({
}) => {
const [currentSchema, setSchema] = useState('');
const [currentTableName, setTableName] = useState('');
- const [datasourceId, setDatasourceId] = useState<number | null>(null);
+ const [datasourceId, setDatasourceId] = useState<number>(0);
const [disableSave, setDisableSave] = useState(true);
const onChange = ({
@@ -128,7 +126,6 @@ const DatasetModal: FunctionComponent<DatasetModalProps> = ({
handleError={addDangerToast}
onChange={onChange}
schema={currentSchema}
- sqlLabMode={false}
tableName={currentTableName}
/>
</TableSelectorContainer>
diff --git a/superset-frontend/stylesheets/superset.less b/superset-frontend/stylesheets/superset.less
index 39305f2..2143fcc 100644
--- a/superset-frontend/stylesheets/superset.less
+++ b/superset-frontend/stylesheets/superset.less
@@ -319,6 +319,10 @@ table.table-no-hover tr:hover {
margin-top: 10px;
}
+.m-t-20 {
+ margin-top: 20px;
+}
+
.m-b-10 {
margin-bottom: 10px;
}
diff --git a/tests/datasets/api_tests.py b/tests/datasets/api_tests.py
index 56f9ef8..b05a54b 100644
--- a/tests/datasets/api_tests.py
+++ b/tests/datasets/api_tests.py
@@ -826,10 +826,6 @@ class TestDatasetApi(SupersetTestCase):
self.login(username="admin")
rv = self.get_assert_metric(uri, "export")
self.assertEqual(rv.status_code, 200)
- self.assertEqual(
- rv.headers["Content-Disposition"],
- generate_download_headers("yaml")["Content-Disposition"],
- )
cli_export = export_to_dict(
session=db.session,