You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by er...@apache.org on 2021/08/26 22:06:01 UTC

[superset] branch revert-16334-change-database-selector-select created (now cce34a5)

This is an automated email from the ASF dual-hosted git repository.

erikrit pushed a change to branch revert-16334-change-database-selector-select
in repository https://gitbox.apache.org/repos/asf/superset.git.


      at cce34a5  Revert "chore: Changes the DatabaseSelector to use the new Select component (#16334)"

This branch includes the following new commits:

     new cce34a5  Revert "chore: Changes the DatabaseSelector to use the new Select component (#16334)"

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[superset] 01/01: Revert "chore: Changes the DatabaseSelector to use the new Select component (#16334)"

Posted by er...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

erikrit pushed a commit to branch revert-16334-change-database-selector-select
in repository https://gitbox.apache.org/repos/asf/superset.git

commit cce34a5dfc510a316f430f0092d4002d341f5ab6
Author: Erik Ritter <er...@airbnb.com>
AuthorDate: Thu Aug 26 15:04:36 2021 -0700

    Revert "chore: Changes the DatabaseSelector to use the new Select component (#16334)"
    
    This reverts commit c768941f2f662e1a0dfa1e1731319d22ec9ca886.
---
 .../javascripts/sqllab/SqlEditorLeftBar_spec.jsx   |  10 +-
 .../src/components/CertifiedIcon/index.tsx         |   9 +-
 .../DatabaseSelector/DatabaseSelector.test.tsx     |  62 ++-
 .../src/components/DatabaseSelector/index.tsx      | 310 ++++++-------
 superset-frontend/src/components/Icons/Icon.tsx    |   8 +-
 superset-frontend/src/components/Select/Select.tsx |  76 ++--
 .../TableSelector/TableSelector.test.jsx           | 291 +++++++++++++
 .../TableSelector/TableSelector.test.tsx           |  91 ----
 .../src/components/TableSelector/index.tsx         | 484 ++++++++++-----------
 .../components/WarningIconWithTooltip/index.tsx    |   6 +-
 .../src/datasource/DatasourceEditor.jsx            | 132 +++---
 .../controls/DatasourceControl/index.jsx           |   5 +-
 .../src/views/CRUD/data/dataset/DatasetList.tsx    |   2 -
 superset/datasets/api.py                           |   2 +-
 superset/views/core.py                             |  21 +-
 tests/integration_tests/datasets/api_tests.py      |  14 +-
 16 files changed, 831 insertions(+), 692 deletions(-)

diff --git a/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx b/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
index b153c14..1ba1ac8 100644
--- a/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
+++ b/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
@@ -81,13 +81,9 @@ describe('Left Panel Expansion', () => {
         </Provider>
       </ThemeProvider>,
     );
-    const dbSelect = screen.getByRole('combobox', {
-      name: 'Select a database',
-    });
-    const schemaSelect = screen.getByRole('combobox', {
-      name: 'Select a schema',
-    });
-    const dropdown = screen.getByText(/Select a table/i);
+    const dbSelect = screen.getByText(/select a database/i);
+    const schemaSelect = screen.getByText(/select a schema \(0\)/i);
+    const dropdown = screen.getByText(/Select table/i);
     const abUser = screen.getByText(/ab_user/i);
     expect(dbSelect).toBeInTheDocument();
     expect(schemaSelect).toBeInTheDocument();
diff --git a/superset-frontend/src/components/CertifiedIcon/index.tsx b/superset-frontend/src/components/CertifiedIcon/index.tsx
index 4aa0dad..f08e9bf 100644
--- a/superset-frontend/src/components/CertifiedIcon/index.tsx
+++ b/superset-frontend/src/components/CertifiedIcon/index.tsx
@@ -18,19 +18,19 @@
  */
 import React from 'react';
 import { t, supersetTheme } from '@superset-ui/core';
-import Icons, { IconType } from 'src/components/Icons';
+import Icons from 'src/components/Icons';
 import { Tooltip } from 'src/components/Tooltip';
 
 export interface CertifiedIconProps {
   certifiedBy?: string;
   details?: string;
-  size?: IconType['iconSize'];
+  size?: number;
 }
 
 function CertifiedIcon({
   certifiedBy,
   details,
-  size = 'l',
+  size = 24,
 }: CertifiedIconProps) {
   return (
     <Tooltip
@@ -48,7 +48,8 @@ function CertifiedIcon({
     >
       <Icons.Certified
         iconColor={supersetTheme.colors.primary.base}
-        iconSize={size}
+        height={size}
+        width={size}
       />
     </Tooltip>
   );
diff --git a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
index 6d4abb3..0d81282 100644
--- a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
+++ b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
@@ -26,11 +26,11 @@ import DatabaseSelector from '.';
 const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
 
 const createProps = () => ({
-  db: { id: 1, database_name: 'test', backend: 'postgresql' },
+  dbId: 1,
   formMode: false,
   isDatabaseSelectEnabled: true,
   readOnly: false,
-  schema: undefined,
+  schema: 'public',
   sqlLabMode: true,
   getDbList: jest.fn(),
   getTableList: jest.fn(),
@@ -129,7 +129,7 @@ beforeEach(() => {
               changed_on: '2021-03-09T19:02:07.141095',
               changed_on_delta_humanized: 'a day ago',
               created_by: null,
-              database_name: 'test',
+              database_name: 'examples',
               explore_database_id: 1,
               expose_in_sqllab: true,
               force_ctas_schema: null,
@@ -153,62 +153,50 @@ test('Refresh should work', async () => {
 
   render(<DatabaseSelector {...props} />);
 
-  const select = screen.getByRole('combobox', {
-    name: 'Select a schema',
-  });
-
-  userEvent.click(select);
-
   await waitFor(() => {
-    expect(SupersetClientGet).toBeCalledTimes(1);
-    expect(props.getDbList).toBeCalledTimes(0);
+    expect(SupersetClientGet).toBeCalledTimes(2);
+    expect(props.getDbList).toBeCalledTimes(1);
     expect(props.getTableList).toBeCalledTimes(0);
     expect(props.handleError).toBeCalledTimes(0);
     expect(props.onDbChange).toBeCalledTimes(0);
     expect(props.onSchemaChange).toBeCalledTimes(0);
-    expect(props.onSchemasLoad).toBeCalledTimes(0);
+    expect(props.onSchemasLoad).toBeCalledTimes(1);
     expect(props.onUpdate).toBeCalledTimes(0);
   });
 
-  userEvent.click(screen.getByRole('button', { name: 'refresh' }));
+  userEvent.click(screen.getByRole('button'));
 
   await waitFor(() => {
-    expect(SupersetClientGet).toBeCalledTimes(2);
-    expect(props.getDbList).toBeCalledTimes(0);
+    expect(SupersetClientGet).toBeCalledTimes(3);
+    expect(props.getDbList).toBeCalledTimes(1);
     expect(props.getTableList).toBeCalledTimes(0);
     expect(props.handleError).toBeCalledTimes(0);
-    expect(props.onDbChange).toBeCalledTimes(0);
-    expect(props.onSchemaChange).toBeCalledTimes(0);
+    expect(props.onDbChange).toBeCalledTimes(1);
+    expect(props.onSchemaChange).toBeCalledTimes(1);
     expect(props.onSchemasLoad).toBeCalledTimes(2);
-    expect(props.onUpdate).toBeCalledTimes(0);
+    expect(props.onUpdate).toBeCalledTimes(1);
   });
 });
 
 test('Should database select display options', async () => {
   const props = createProps();
   render(<DatabaseSelector {...props} />);
-  const select = screen.getByRole('combobox', {
-    name: 'Select a database',
-  });
-  expect(select).toBeInTheDocument();
-  userEvent.click(select);
-  expect(
-    await screen.findByRole('option', { name: 'postgresql: test' }),
-  ).toBeInTheDocument();
+  const selector = await screen.findByText('Database:');
+  expect(selector).toBeInTheDocument();
+  expect(selector.parentElement).toHaveTextContent(
+    'Database:postgresql examples',
+  );
 });
 
 test('Should schema select display options', async () => {
   const props = createProps();
   render(<DatabaseSelector {...props} />);
-  const select = screen.getByRole('combobox', {
-    name: 'Select a schema',
-  });
-  expect(select).toBeInTheDocument();
-  userEvent.click(select);
-  expect(
-    await screen.findByRole('option', { name: 'public' }),
-  ).toBeInTheDocument();
-  expect(
-    await screen.findByRole('option', { name: 'information_schema' }),
-  ).toBeInTheDocument();
+
+  const selector = await screen.findByText('Schema:');
+  expect(selector).toBeInTheDocument();
+  expect(selector.parentElement).toHaveTextContent('Schema: public');
+
+  userEvent.click(screen.getByRole('button'));
+
+  expect(await screen.findByText('Select a schema (2)')).toBeInTheDocument();
 });
diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx b/superset-frontend/src/components/DatabaseSelector/index.tsx
index c96fba7..0282e4a 100644
--- a/superset-frontend/src/components/DatabaseSelector/index.tsx
+++ b/superset-frontend/src/components/DatabaseSelector/index.tsx
@@ -16,51 +16,58 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { ReactNode, useState, useMemo } from 'react';
+import React, { ReactNode, useEffect, useState } from 'react';
 import { styled, SupersetClient, t } from '@superset-ui/core';
 import rison from 'rison';
-import { Select } from 'src/components';
-import { FormLabel } from 'src/components/Form';
+import { Select } from 'src/components/Select';
+import Label from 'src/components/Label';
 import RefreshLabel from 'src/components/RefreshLabel';
+import SupersetAsyncSelect from 'src/components/AsyncSelect';
+
+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`
-  ${({ theme }) => `
-    .refresh {
-      display: flex;
-      align-items: center;
-      width: 30px;
-      margin-left: ${theme.gridUnit}px;
-      margin-top: ${theme.gridUnit * 5}px;
-    }
+  .fa-refresh {
+    padding-left: 9px;
+  }
 
-    .section {
-      display: flex;
-      flex-direction: row;
-      align-items: center;
-    }
+  .refresh-col {
+    display: flex;
+    align-items: center;
+    width: 30px;
+    margin-left: ${({ theme }) => theme.gridUnit}px;
+  }
 
-    .select {
-      flex: 1;
-    }
+  .section {
+    padding-bottom: 5px;
+    display: flex;
+    flex-direction: row;
+  }
 
-    & > div {
-      margin-bottom: ${theme.gridUnit * 4}px;
-    }
-  `}
+  .select {
+    flex-grow: 1;
+  }
 `;
 
-type DatabaseValue = { label: string; value: number };
-
-type SchemaValue = { label: string; value: string };
+const DatabaseOption = styled.span`
+  display: inline-flex;
+  align-items: center;
+`;
 
 interface DatabaseSelectorProps {
-  db?: { id: number; database_name: string; backend: string };
+  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?: (schema?: string) => void;
+  onSchemaChange?: (arg0?: any) => {};
   onSchemasLoad?: (schemas: Array<object>) => void;
   readOnly?: boolean;
   schema?: string;
@@ -76,9 +83,10 @@ interface DatabaseSelectorProps {
 }
 
 export default function DatabaseSelector({
-  db,
+  dbId,
   formMode = false,
   getDbList,
+  getTableList,
   handleError,
   isDatabaseSelectEnabled = true,
   onUpdate,
@@ -89,189 +97,193 @@ export default function DatabaseSelector({
   schema,
   sqlLabMode = false,
 }: DatabaseSelectorProps) {
-  const [currentDb, setCurrentDb] = useState(
-    db
-      ? { label: `${db.backend}: ${db.database_name}`, value: db.id }
-      : undefined,
-  );
-  const [currentSchema, setCurrentSchema] = useState<SchemaValue | undefined>(
-    schema ? { label: schema, value: schema } : undefined,
+  const [currentDbId, setCurrentDbId] = useState(dbId);
+  const [currentSchema, setCurrentSchema] = useState<string | undefined>(
+    schema,
   );
-  const [refresh, setRefresh] = useState(0);
+  const [schemaLoading, setSchemaLoading] = useState(false);
+  const [schemaOptions, setSchemaOptions] = useState([]);
 
-  const loadSchemas = useMemo(
-    () => async (): Promise<{
-      data: SchemaValue[];
-      totalCount: number;
-    }> => {
-      if (currentDb) {
-        const queryParams = rison.encode({ force: refresh > 0 });
-        const endpoint = `/api/v1/database/${currentDb.value}/schemas/?q=${queryParams}`;
-
-        // TODO: Would be nice to add pagination in a follow-up. Needs endpoint changes.
-        return SupersetClient.get({ endpoint }).then(({ json }) => {
+  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);
           }
-          return {
-            data: options,
-            totalCount: options.length,
-          };
+        })
+        .catch(() => {
+          setSchemaOptions([]);
+          setSchemaLoading(false);
+          handleError(t('Error while fetching schema list'));
         });
-      }
-      return {
-        data: [],
-        totalCount: 0,
-      };
-    },
-    [currentDb, refresh, onSchemasLoad],
-  );
+    }
+    return Promise.resolve();
+  }
 
-  function onSelectChange({
-    db,
-    schema,
-  }: {
-    db: DatabaseValue;
-    schema?: SchemaValue;
-  }) {
-    setCurrentDb(db);
+  useEffect(() => {
+    if (currentDbId) {
+      fetchSchemas(currentDbId);
+    }
+  }, [currentDbId]);
+
+  function onSelectChange({ dbId, schema }: { dbId: number; schema?: string }) {
+    setCurrentDbId(dbId);
     setCurrentSchema(schema);
     if (onUpdate) {
-      onUpdate({
-        dbId: db.value,
-        schema: schema?.value,
-        tableName: undefined,
-      });
+      onUpdate({ dbId, schema, tableName: undefined });
     }
   }
 
-  function changeDataBase(selectedValue: DatabaseValue) {
-    const actualDb = selectedValue || db;
+  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(undefined);
+      onSchemaChange(null);
     }
     if (onDbChange) {
       onDbChange(db);
     }
-    onSelectChange({ db: actualDb, schema: undefined });
+    fetchSchemas(dbId, force);
+    onSelectChange({ dbId, schema: undefined });
   }
 
-  function changeSchema(schema: SchemaValue) {
+  function changeSchema(schemaOpt: any, force = false) {
+    const schema = schemaOpt ? schemaOpt.value : null;
     if (onSchemaChange) {
-      onSchemaChange(schema.value);
+      onSchemaChange(schema);
     }
-    if (currentDb) {
-      onSelectChange({ db: currentDb, schema });
+    setCurrentSchema(schema);
+    onSelectChange({ dbId: currentDbId, schema });
+    if (getTableList) {
+      getTableList(currentDbId, schema, force);
     }
   }
 
+  function renderDatabaseOption(db: any) {
+    return (
+      <DatabaseOption title={db.database_name}>
+        <Label type="default">{db.backend}</Label> {db.database_name}
+      </DatabaseOption>
+    );
+  }
+
   function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
     return (
       <div className="section">
         <span className="select">{select}</span>
-        <span className="refresh">{refreshBtn}</span>
+        <span className="refresh-col">{refreshBtn}</span>
       </div>
     );
   }
 
-  const loadDatabases = useMemo(
-    () => async (
-      search: string,
-      page: number,
-      pageSize: number,
-    ): Promise<{
-      data: DatabaseValue[];
-      totalCount: number;
-    }> => {
-      const queryParams = rison.encode({
-        order_columns: 'database_name',
-        order_direction: 'asc',
-        page,
-        page_size: pageSize,
-        ...(formMode || !sqlLabMode
-          ? { filters: [{ col: 'database_name', opr: 'ct', value: search }] }
-          : {
-              filters: [
-                { col: 'database_name', opr: 'ct', value: search },
-                {
-                  col: 'expose_in_sqllab',
-                  opr: 'eq',
-                  value: true,
-                },
-              ],
-            }),
-      });
-      const endpoint = `/api/v1/database/?q=${queryParams}`;
-      return SupersetClient.get({ endpoint }).then(({ json }) => {
-        const { result } = json;
-        if (getDbList) {
-          getDbList(result);
-        }
-        if (result.length === 0) {
-          handleError(t("It seems you don't have access to any database"));
-        }
-        const options = result.map(
-          (row: { backend: string; database_name: string; id: number }) => ({
-            label: `${row.backend}: ${row.database_name}`,
-            value: row.id,
+  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 {
-          data: options,
-          totalCount: options.length,
-        };
-      });
-    },
-    [formMode, getDbList, handleError, sqlLabMode],
-  );
+    });
 
-  function renderDatabaseSelect() {
     return renderSelectRow(
-      <Select
-        ariaLabel={t('Select a database')}
+      <SupersetAsyncSelect
         data-test="select-database"
-        header={<FormLabel>{t('Database')}</FormLabel>}
-        onChange={changeDataBase}
-        value={currentDb}
+        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')}
-        disabled={!isDatabaseSelectEnabled || readOnly}
-        options={loadDatabases}
+        autoSelect
+        isDisabled={!isDatabaseSelectEnabled || readOnly}
       />,
       null,
     );
   }
 
   function renderSchemaSelect() {
-    const refreshIcon = !formMode && !readOnly && (
+    const value = schemaOptions.filter(({ value }) => currentSchema === value);
+    const refresh = !formMode && !readOnly && (
       <RefreshLabel
-        onClick={() => setRefresh(refresh + 1)}
+        onClick={() => changeDataBase({ id: dbId }, true)}
         tooltipContent={t('Force refresh schema list')}
       />
     );
 
     return renderSelectRow(
       <Select
-        ariaLabel={t('Select a schema')}
-        disabled={readOnly}
-        header={<FormLabel>{t('Schema')}</FormLabel>}
         name="select-schema"
-        placeholder={t('Select a schema')}
-        onChange={item => changeSchema(item as SchemaValue)}
-        options={loadSchemas}
-        value={currentSchema}
+        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)}
+        isDisabled={readOnly}
       />,
-      refreshIcon,
+      refresh,
     );
   }
 
   return (
     <DatabaseSelectorWrapper data-test="DatabaseSelector">
+      {formMode && <FieldTitle>{t('datasource')}</FieldTitle>}
       {renderDatabaseSelect()}
+      {formMode && <FieldTitle>{t('schema')}</FieldTitle>}
       {renderSchemaSelect()}
     </DatabaseSelectorWrapper>
   );
diff --git a/superset-frontend/src/components/Icons/Icon.tsx b/superset-frontend/src/components/Icons/Icon.tsx
index efb78dc..9e3d0e1 100644
--- a/superset-frontend/src/components/Icons/Icon.tsx
+++ b/superset-frontend/src/components/Icons/Icon.tsx
@@ -53,21 +53,15 @@ export const Icon = (props: IconProps) => {
   const name = fileName.replace('_', '-');
 
   useEffect(() => {
-    let cancelled = false;
     async function importIcon(): Promise<void> {
       ImportedSVG.current = (
         await import(
           `!!@svgr/webpack?-svgo,+titleProp,+ref!images/icons/${fileName}.svg`
         )
       ).default;
-      if (!cancelled) {
-        setLoaded(true);
-      }
+      setLoaded(true);
     }
     importIcon();
-    return () => {
-      cancelled = true;
-    };
   }, [fileName, ImportedSVG]);
 
   return (
diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx
index 96eb79b..596722e 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -86,9 +86,12 @@ const StyledContainer = styled.div`
   flex-direction: column;
 `;
 
-const StyledSelect = styled(AntdSelect)`
-  ${({ theme }) => `
+const StyledSelect = styled(AntdSelect, {
+  shouldForwardProp: prop => prop !== 'hasHeader',
+})<{ hasHeader: boolean }>`
+  ${({ theme, hasHeader }) => `
     width: 100%;
+    margin-top: ${hasHeader ? theme.gridUnit : 0}px;
 
     && .ant-select-selector {
       border-radius: ${theme.gridUnit}px;
@@ -186,7 +189,6 @@ const Select = ({
     : 'multiple';
 
   useEffect(() => {
-    fetchedQueries.current.clear();
     setSelectOptions(
       options && Array.isArray(options) ? options : EMPTY_OPTIONS,
     );
@@ -364,45 +366,34 @@ const Select = ({
     [options],
   );
 
-  const handleOnSearch = useMemo(
-    () =>
-      debounce((search: string) => {
-        const searchValue = search.trim();
-        // enables option creation
-        if (allowNewOptions && isSingleMode) {
-          const firstOption =
-            selectOptions.length > 0 && selectOptions[0].value;
-          // replaces the last search value entered with the new one
-          // only when the value wasn't part of the original options
-          if (
-            searchValue &&
-            firstOption === searchedValue &&
-            !initialOptions.find(o => o.value === searchedValue)
-          ) {
-            selectOptions.shift();
-            setSelectOptions(selectOptions);
-          }
-          if (searchValue && !hasOption(searchValue, selectOptions)) {
-            const newOption = {
-              label: searchValue,
-              value: searchValue,
-            };
-            // adds a custom option
-            const newOptions = [...selectOptions, newOption];
-            setSelectOptions(newOptions);
-            setSelectValue(searchValue);
-          }
-        }
-        setSearchedValue(searchValue);
-      }, DEBOUNCE_TIMEOUT),
-    [
-      allowNewOptions,
-      initialOptions,
-      isSingleMode,
-      searchedValue,
-      selectOptions,
-    ],
-  );
+  const handleOnSearch = debounce((search: string) => {
+    const searchValue = search.trim();
+    // enables option creation
+    if (allowNewOptions && isSingleMode) {
+      const firstOption = selectOptions.length > 0 && selectOptions[0].value;
+      // replaces the last search value entered with the new one
+      // only when the value wasn't part of the original options
+      if (
+        searchValue &&
+        firstOption === searchedValue &&
+        !initialOptions.find(o => o.value === searchedValue)
+      ) {
+        selectOptions.shift();
+        setSelectOptions(selectOptions);
+      }
+      if (searchValue && !hasOption(searchValue, selectOptions)) {
+        const newOption = {
+          label: searchValue,
+          value: searchValue,
+        };
+        // adds a custom option
+        const newOptions = [...selectOptions, newOption];
+        setSelectOptions(newOptions);
+        setSelectValue(searchValue);
+      }
+    }
+    setSearchedValue(searchValue);
+  }, DEBOUNCE_TIMEOUT);
 
   const handlePagination = (e: UIEvent<HTMLElement>) => {
     const vScroll = e.currentTarget;
@@ -495,6 +486,7 @@ const Select = ({
     <StyledContainer>
       {header}
       <StyledSelect
+        hasHeader={!!header}
         aria-label={ariaLabel || name}
         dropdownRender={dropdownRender}
         filterOption={handleFilterOption}
diff --git a/superset-frontend/src/components/TableSelector/TableSelector.test.jsx b/superset-frontend/src/components/TableSelector/TableSelector.test.jsx
new file mode 100644
index 0000000..e1fa66b
--- /dev/null
+++ b/superset-frontend/src/components/TableSelector/TableSelector.test.jsx
@@ -0,0 +1,291 @@
+/**
+ * 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 configureStore from 'redux-mock-store';
+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 'spec/javascripts/sqllab/fixtures';
+
+const mockStore = configureStore([thunk]);
+const store = mockStore(initialState);
+
+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/*';
+
+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,
+};
+
+const schemaOptions = {
+  result: ['main', 'erf', 'superset'],
+};
+const selectedSchema = { label: 'main', title: 'main', value: 'main' };
+const selectedTable = {
+  extra: null,
+  label: 'birth_names',
+  schema: 'main',
+  title: 'birth_names',
+  type: undefined,
+  value: 'birth_names',
+};
+
+async function mountAndWait(props = mockedProps) {
+  const mounted = mount(<TableSelector {...props} />, {
+    context: { store },
+    wrappingComponent: ThemeProvider,
+    wrappingComponentProps: { theme: supersetTheme },
+  });
+  await waitForComponentToPaint(mounted);
+
+  return mounted;
+}
+
+describe('TableSelector', () => {
+  let wrapper;
+
+  beforeEach(async () => {
+    fetchMock.reset();
+    wrapper = await mountAndWait();
+  });
+
+  it('renders', () => {
+    expect(wrapper.find(TableSelector)).toExist();
+    expect(wrapper.find(DatabaseSelector)).toExist();
+  });
+
+  describe('change database', () => {
+    afterEach(fetchMock.resetHistory);
+    afterAll(fetchMock.reset);
+
+    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 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);
+
+      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' },
+      ]);
+    });
+
+    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('change schema', () => {
+    beforeEach(async () => {
+      fetchMock.get(FETCH_SCHEMAS_ENDPOINT, schemaOptions, {
+        overwriteRoutes: true,
+      });
+    });
+
+    afterEach(fetchMock.resetHistory);
+    afterAll(fetchMock.reset);
+
+    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);
+    });
+
+    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('change table', () => {
+    beforeEach(async () => {
+      fetchMock.get(GET_TABLE_NAMES_ENDPOINT, tables, {
+        overwriteRoutes: true,
+      });
+    });
+
+    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);
+      });
+      await waitForComponentToPaint(wrapper);
+      expect(
+        wrapper.find('[name="select-table"]').first().props().value,
+      ).toEqual('birth_names');
+    });
+
+    it('should call onTableChange with schema from table object', async () => {
+      act(() => {
+        wrapper
+          .find('[name="select-schema"]')
+          .first()
+          .props()
+          .onChange(selectedSchema);
+      });
+      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');
+    });
+  });
+
+  describe('getTableNamesBySubStr', () => {
+    afterEach(fetchMock.resetHistory);
+    afterAll(fetchMock.reset);
+
+    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('');
+    });
+
+    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/src/components/TableSelector/TableSelector.test.tsx b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx
deleted file mode 100644
index 3b8b617..0000000
--- a/superset-frontend/src/components/TableSelector/TableSelector.test.tsx
+++ /dev/null
@@ -1,91 +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 { render, screen, waitFor } from 'spec/helpers/testing-library';
-import { SupersetClient } from '@superset-ui/core';
-import userEvent from '@testing-library/user-event';
-import TableSelector from '.';
-
-const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
-
-const createProps = () => ({
-  dbId: 1,
-  schema: 'test_schema',
-  handleError: jest.fn(),
-});
-
-beforeAll(() => {
-  SupersetClientGet.mockImplementation(
-    async () =>
-      ({
-        json: {
-          options: [
-            { label: 'table_a', value: 'table_a' },
-            { label: 'table_b', value: 'table_b' },
-          ],
-        },
-      } as any),
-  );
-});
-
-test('renders with default props', async () => {
-  const props = createProps();
-  render(<TableSelector {...props} />);
-  const databaseSelect = screen.getByRole('combobox', {
-    name: 'Select a database',
-  });
-  const schemaSelect = screen.getByRole('combobox', {
-    name: 'Select a database',
-  });
-  const tableSelect = screen.getByRole('combobox', {
-    name: 'Select a table',
-  });
-  await waitFor(() => {
-    expect(databaseSelect).toBeInTheDocument();
-    expect(schemaSelect).toBeInTheDocument();
-    expect(tableSelect).toBeInTheDocument();
-  });
-});
-
-test('renders table options', async () => {
-  const props = createProps();
-  render(<TableSelector {...props} />);
-  const tableSelect = screen.getByRole('combobox', {
-    name: 'Select a table',
-  });
-  userEvent.click(tableSelect);
-  expect(
-    await screen.findByRole('option', { name: 'table_a' }),
-  ).toBeInTheDocument();
-  expect(
-    await screen.findByRole('option', { name: 'table_b' }),
-  ).toBeInTheDocument();
-});
-
-test('renders disabled without schema', async () => {
-  const props = createProps();
-  render(<TableSelector {...props} schema={undefined} />);
-  const tableSelect = screen.getByRole('combobox', {
-    name: 'Select a table',
-  });
-  await waitFor(() => {
-    expect(tableSelect).toBeDisabled();
-  });
-});
diff --git a/superset-frontend/src/components/TableSelector/index.tsx b/superset-frontend/src/components/TableSelector/index.tsx
index 5f68a94..c437b1e 100644
--- a/superset-frontend/src/components/TableSelector/index.tsx
+++ b/superset-frontend/src/components/TableSelector/index.tsx
@@ -18,49 +18,57 @@
  */
 import React, {
   FunctionComponent,
+  useEffect,
   useState,
   ReactNode,
-  useMemo,
-  useEffect,
 } from 'react';
 import { styled, SupersetClient, t } from '@superset-ui/core';
-import { Select } from 'src/components';
+import { AsyncSelect, CreatableSelect, Select } from 'src/components/Select';
+
 import { FormLabel } from 'src/components/Form';
-import Icons from 'src/components/Icons';
+
 import DatabaseSelector from 'src/components/DatabaseSelector';
 import RefreshLabel from 'src/components/RefreshLabel';
 import CertifiedIcon from 'src/components/CertifiedIcon';
 import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip';
 
+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`
-  ${({ theme }) => `
-    .refresh {
-      display: flex;
-      align-items: center;
-      width: 30px;
-      margin-left: ${theme.gridUnit}px;
-      margin-top: ${theme.gridUnit * 5}px;
-    }
+  .fa-refresh {
+    padding-left: 9px;
+  }
 
-    .section {
-      display: flex;
-      flex-direction: row;
-      align-items: center;
-    }
+  .refresh-col {
+    display: flex;
+    align-items: center;
+    width: 30px;
+    margin-left: ${({ theme }) => theme.gridUnit}px;
+  }
 
-    .divider {
-      border-bottom: 1px solid ${theme.colors.secondary.light5};
-      margin: 15px 0;
-    }
+  .section {
+    padding-bottom: 5px;
+    display: flex;
+    flex-direction: row;
+  }
 
-    .table-length {
-      color: ${theme.colors.grayscale.light1};
-    }
+  .select {
+    flex-grow: 1;
+  }
 
-    .select {
-      flex: 1;
-    }
-  `}
+  .divider {
+    border-bottom: 1px solid ${({ theme }) => theme.colors.secondary.light5};
+    margin: 15px 0;
+  }
+
+  .table-length {
+    color: ${({ theme }) => theme.colors.grayscale.light1};
+  }
 `;
 
 const TableLabel = styled.span`
@@ -90,15 +98,7 @@ interface TableSelectorProps {
     schema?: string;
     tableName?: string;
   }) => void;
-  onDbChange?: (
-    db:
-      | {
-          id: number;
-          database_name: string;
-          backend: string;
-        }
-      | undefined,
-  ) => void;
+  onDbChange?: (db: any) => void;
   onSchemaChange?: (arg0?: any) => {};
   onSchemasLoad?: () => void;
   onTableChange?: (tableName: string, schema: string) => void;
@@ -110,52 +110,6 @@ interface TableSelectorProps {
   tableNameSticky?: boolean;
 }
 
-interface Table {
-  label: string;
-  value: string;
-  type: string;
-  extra?: {
-    certification?: {
-      certified_by: string;
-      details: string;
-    };
-    warning_markdown?: string;
-  };
-}
-
-interface TableOption {
-  label: JSX.Element;
-  text: string;
-  value: string;
-}
-
-const TableOption = ({ table }: { table: Table }) => {
-  const { label, type, extra } = table;
-  return (
-    <TableLabel title={label}>
-      {type === 'view' ? (
-        <Icons.Eye iconSize="m" />
-      ) : (
-        <Icons.Table iconSize="m" />
-      )}
-      {extra?.certification && (
-        <CertifiedIcon
-          certifiedBy={extra.certification.certified_by}
-          details={extra.certification.details}
-          size="l"
-        />
-      )}
-      {extra?.warning_markdown && (
-        <WarningIconWithTooltip
-          warningMarkdown={extra.warning_markdown}
-          size="l"
-        />
-      )}
-      {label}
-    </TableLabel>
-  );
-};
-
 const TableSelector: FunctionComponent<TableSelectorProps> = ({
   database,
   dbId,
@@ -175,187 +129,179 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
   tableName,
   tableNameSticky = true,
 }) => {
-  const [currentDbId, setCurrentDbId] = useState<number | undefined>(dbId);
   const [currentSchema, setCurrentSchema] = useState<string | undefined>(
     schema,
   );
-  const [currentTable, setCurrentTable] = useState<TableOption | undefined>();
-  const [refresh, setRefresh] = useState(0);
-  const [previousRefresh, setPreviousRefresh] = useState(0);
-
-  const loadTable = useMemo(
-    () => async (dbId: number, schema: string, tableName: string) => {
+  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/${dbId}/${schema}/${encodeURIComponent(
-          tableName,
-        )}/false/true`,
+        `/superset/tables/${actualDbId}/${encodedSchema}/${encodedSubstr}/${!!forceRefresh}/`,
       );
-
-      if (previousRefresh !== refresh) {
-        setPreviousRefresh(refresh);
-      }
-
-      return SupersetClient.get({ endpoint }).then(({ json }) => {
-        const options = json.options as Table[];
-        if (options && options.length > 0) {
-          return options[0];
-        }
-        return null;
-      });
-    },
-    [], // eslint-disable-line react-hooks/exhaustive-deps
-  );
-
-  const loadTables = useMemo(
-    () => async (search: string) => {
-      const dbSchema = schema || currentSchema;
-      if (currentDbId && dbSchema) {
-        const encodedSchema = encodeURIComponent(dbSchema);
-        const encodedSubstr = encodeURIComponent(search || 'undefined');
-        const forceRefresh = refresh !== previousRefresh;
-        const endpoint = encodeURI(
-          `/superset/tables/${currentDbId}/${encodedSchema}/${encodedSubstr}/${forceRefresh}/`,
-        );
-
-        if (previousRefresh !== refresh) {
-          setPreviousRefresh(refresh);
-        }
-
-        return SupersetClient.get({ endpoint }).then(({ json }) => {
-          const options = json.options
-            .map((table: Table) => ({
-              value: table.value,
-              label: <TableOption table={table} />,
-              text: table.label,
-            }))
-            .sort((a: { text: string }, b: { text: string }) =>
-              a.text.localeCompare(b.text),
-            );
-
+      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,
+            extra: o?.extra,
+          }));
+          setTableLoading(false);
+          setTableOptions(options);
           if (onTablesLoad) {
             onTablesLoad(json.options);
           }
-
-          return {
-            data: options,
-            totalCount: options.length,
-          };
+        })
+        .catch(() => {
+          setTableLoading(false);
+          setTableOptions([]);
+          handleError(t('Error while fetching table list'));
         });
-      }
-      return { data: [], totalCount: 0 };
-    },
-    // We are using the refresh state to re-trigger the query
-    // previousRefresh should be out of dependencies array
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [currentDbId, currentSchema, onTablesLoad, schema, refresh],
-  );
+    }
+    setTableLoading(false);
+    setTableOptions([]);
+    return Promise.resolve();
+  }
 
   useEffect(() => {
-    async function fetchTable() {
-      if (schema && tableName) {
-        const table = await loadTable(dbId, schema, tableName);
-        if (table) {
-          setCurrentTable({
-            label: <TableOption table={table} />,
-            text: table.label,
-            value: table.value,
-          });
-        }
-      }
+    if (dbId && schema) {
+      fetchTables();
     }
-    fetchTable();
-  }, []); // eslint-disable-line react-hooks/exhaustive-deps
+  }, [dbId, schema]);
 
   function onSelectionChange({
     dbId,
     schema,
-    table,
+    tableName,
   }: {
     dbId: number;
     schema?: string;
-    table?: TableOption;
+    tableName?: string;
   }) {
-    setCurrentTable(table);
-    setCurrentDbId(dbId);
+    setCurrentTableName(tableName);
     setCurrentSchema(schema);
     if (onUpdate) {
-      onUpdate({ dbId, schema, tableName: table?.value });
+      onUpdate({ 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(table: TableOption) {
-    if (!table) {
-      setCurrentTable(undefined);
+  function changeTable(tableOpt: any) {
+    if (!tableOpt) {
+      setCurrentTableName('');
       return;
     }
-    const tableOptTableName = table.value;
-    if (currentDbId && tableNameSticky) {
+    const schemaName = tableOpt.schema;
+    const tableOptTableName = tableOpt.value;
+    if (tableNameSticky) {
       onSelectionChange({
-        dbId: currentDbId,
-        schema: currentSchema,
-        table,
+        dbId,
+        schema: schemaName,
+        tableName: tableOptTableName,
       });
     }
-    if (onTableChange && currentSchema) {
-      onTableChange(tableOptTableName, currentSchema);
+    if (onTableChange) {
+      onTableChange(tableOptTableName, schemaName);
     }
   }
 
-  function onRefresh() {
+  function changeSchema(schemaOpt: any, force = false) {
+    const value = schemaOpt ? schemaOpt.value : null;
     if (onSchemaChange) {
-      onSchemaChange(currentSchema);
+      onSchemaChange(value);
     }
-    if (currentDbId && currentSchema) {
-      onSelectionChange({
-        dbId: currentDbId,
-        schema: currentSchema,
-        table: currentTable,
-      });
-    }
-    setRefresh(refresh + 1);
+    onSelectionChange({
+      dbId,
+      schema: value,
+      tableName: undefined,
+    });
+    fetchTables(dbId, currentSchema, force);
+  }
+
+  function renderTableOption(option: any) {
+    return (
+      <TableLabel title={option.label}>
+        <small className="text-muted">
+          <i className={`fa fa-${option.type === 'view' ? 'eye' : 'table'}`} />
+        </small>
+        {option.extra?.certification && (
+          <CertifiedIcon
+            certifiedBy={option.extra.certification.certified_by}
+            details={option.extra.certification.details}
+            size={20}
+          />
+        )}
+        {option.extra?.warning_markdown && (
+          <WarningIconWithTooltip
+            warningMarkdown={option.extra.warning_markdown}
+            size={20}
+          />
+        )}
+        {option.label}
+      </TableLabel>
+    );
   }
 
   function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
     return (
       <div className="section">
         <span className="select">{select}</span>
-        <span className="refresh">{refreshBtn}</span>
+        <span className="refresh-col">{refreshBtn}</span>
       </div>
     );
   }
 
-  const internalDbChange = (
-    db:
-      | {
-          id: number;
-          database_name: string;
-          backend: string;
-        }
-      | undefined,
-  ) => {
-    setCurrentDbId(db?.id);
-    if (onDbChange) {
-      onDbChange(db);
-    }
-  };
-
-  const internalSchemaChange = (schema?: string) => {
-    setCurrentSchema(schema);
-    if (onSchemaChange) {
-      onSchemaChange(schema);
-    }
-  };
-
   function renderDatabaseSelector() {
     return (
       <DatabaseSelector
-        db={database}
+        dbId={dbId}
         formMode={formMode}
         getDbList={getDbList}
+        getTableList={fetchTables}
         handleError={handleError}
         onUpdate={onSelectionChange}
-        onDbChange={readOnly ? undefined : internalDbChange}
-        onSchemaChange={readOnly ? undefined : internalSchemaChange}
+        onDbChange={readOnly ? undefined : onDbChange}
+        onSchemaChange={readOnly ? undefined : onSchemaChange}
         onSchemasLoad={onSchemasLoad}
         schema={currentSchema}
         sqlLabMode={sqlLabMode}
@@ -365,54 +311,96 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
     );
   }
 
-  const handleFilterOption = useMemo(
-    () => (search: string, option: TableOption) => {
-      const searchValue = search.trim().toLowerCase();
-      const { text } = option;
-      return text.toLowerCase().includes(searchValue);
-    },
-    [],
-  );
-
   function renderTableSelect() {
-    const disabled =
-      (currentSchema && !formMode && readOnly) ||
-      (!currentSchema && !database?.allow_multi_schema_metadata_fetch);
-
-    const header = sqlLabMode ? (
-      <FormLabel>{t('See table schema')}</FormLabel>
-    ) : (
-      <FormLabel>{t('Table')}</FormLabel>
-    );
-
-    const select = (
-      <Select
-        ariaLabel={t('Select a table')}
-        disabled={disabled}
-        filterOption={handleFilterOption}
-        header={header}
-        name="select-table"
-        onChange={changeTable}
-        options={loadTables}
-        placeholder={t('Select a table')}
-        value={currentTable}
-      />
-    );
-
+    const options = tableOptions;
+    let select = null;
+    if (currentSchema && !formMode) {
+      // dataset editor
+      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}
+          valueRenderer={renderTableOption}
+          isDisabled={readOnly}
+        />
+      );
+    } 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 {
+      // sql lab
+      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;
+      }
+      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 && !readOnly && (
       <RefreshLabel
-        onClick={onRefresh}
+        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 className="table-length">
+              {tableOptions.length} in {schema}
+            </small>
+          )}
+        </FormLabel>
+      </div>
+    );
+  }
+
   return (
     <TableSelectorWrapper>
       {renderDatabaseSelector()}
-      {sqlLabMode && !formMode && <div className="divider" />}
+      {!formMode && <div className="divider" />}
+      {sqlLabMode && renderSeeTableLabel()}
+      {formMode && <FieldTitle>{t('Table')}</FieldTitle>}
       {renderTableSelect()}
     </TableSelectorWrapper>
   );
diff --git a/superset-frontend/src/components/WarningIconWithTooltip/index.tsx b/superset-frontend/src/components/WarningIconWithTooltip/index.tsx
index f732554..f160ade 100644
--- a/superset-frontend/src/components/WarningIconWithTooltip/index.tsx
+++ b/superset-frontend/src/components/WarningIconWithTooltip/index.tsx
@@ -18,17 +18,16 @@
  */
 import React from 'react';
 import { useTheme, SafeMarkdown } from '@superset-ui/core';
-import Icons, { IconType } from 'src/components/Icons';
+import Icons from 'src/components/Icons';
 import { Tooltip } from 'src/components/Tooltip';
 
 export interface WarningIconWithTooltipProps {
   warningMarkdown: string;
-  size?: IconType['iconSize'];
+  size?: number;
 }
 
 function WarningIconWithTooltip({
   warningMarkdown,
-  size,
 }: WarningIconWithTooltipProps) {
   const theme = useTheme();
   return (
@@ -38,7 +37,6 @@ function WarningIconWithTooltip({
     >
       <Icons.AlertSolid
         iconColor={theme.colors.alert.base}
-        iconSize={size}
         css={{ marginRight: theme.gridUnit * 2 }}
       />
     </Tooltip>
diff --git a/superset-frontend/src/datasource/DatasourceEditor.jsx b/superset-frontend/src/datasource/DatasourceEditor.jsx
index d8a0a342..e11b831 100644
--- a/superset-frontend/src/datasource/DatasourceEditor.jsx
+++ b/superset-frontend/src/datasource/DatasourceEditor.jsx
@@ -775,47 +775,41 @@ class DatasourceEditor extends React.PureComponent {
             <div>
               {this.state.isSqla && (
                 <>
-                  <Col xs={24} md={12}>
-                    <Field
-                      fieldKey="databaseSelector"
-                      label={t('virtual')}
-                      control={
-                        <div css={{ marginTop: 8 }}>
-                          <DatabaseSelector
-                            db={datasource?.database}
-                            schema={datasource.schema}
-                            onSchemaChange={schema =>
-                              this.state.isEditMode &&
-                              this.onDatasourcePropChange('schema', schema)
-                            }
-                            onDbChange={database =>
-                              this.state.isEditMode &&
-                              this.onDatasourcePropChange('database', database)
-                            }
-                            formMode={false}
-                            handleError={this.props.addDangerToast}
-                            readOnly={!this.state.isEditMode}
-                          />
-                        </div>
-                      }
-                    />
-                    <div css={{ width: 'calc(100% - 34px)', marginTop: -16 }}>
-                      <Field
-                        fieldKey="table_name"
-                        label={t('Dataset name')}
-                        control={
-                          <TextControl
-                            controlId="table_name"
-                            onChange={table => {
-                              this.onDatasourcePropChange('table_name', table);
-                            }}
-                            placeholder={t('Dataset name')}
-                            disabled={!this.state.isEditMode}
-                          />
+                  <Field
+                    fieldKey="databaseSelector"
+                    label={t('virtual')}
+                    control={
+                      <DatabaseSelector
+                        dbId={datasource.database.id}
+                        schema={datasource.schema}
+                        onSchemaChange={schema =>
+                          this.state.isEditMode &&
+                          this.onDatasourcePropChange('schema', schema)
+                        }
+                        onDbChange={database =>
+                          this.state.isEditMode &&
+                          this.onDatasourcePropChange('database', database)
                         }
+                        formMode={false}
+                        handleError={this.props.addDangerToast}
+                        readOnly={!this.state.isEditMode}
                       />
-                    </div>
-                  </Col>
+                    }
+                  />
+                  <Field
+                    fieldKey="table_name"
+                    label={t('Dataset name')}
+                    control={
+                      <TextControl
+                        controlId="table_name"
+                        onChange={table => {
+                          this.onDatasourcePropChange('table_name', table);
+                        }}
+                        placeholder={t('Dataset name')}
+                        disabled={!this.state.isEditMode}
+                      />
+                    }
+                  />
                   <Field
                     fieldKey="sql"
                     label={t('SQL')}
@@ -859,39 +853,33 @@ class DatasourceEditor extends React.PureComponent {
                   fieldKey="tableSelector"
                   label={t('Physical')}
                   control={
-                    <div css={{ marginTop: 8 }}>
-                      <TableSelector
-                        clearable={false}
-                        database={datasource.database}
-                        dbId={datasource.database.id}
-                        handleError={this.props.addDangerToast}
-                        schema={datasource.schema}
-                        sqlLabMode={false}
-                        tableName={datasource.table_name}
-                        onSchemaChange={
-                          this.state.isEditMode
-                            ? schema =>
-                                this.onDatasourcePropChange('schema', schema)
-                            : undefined
-                        }
-                        onDbChange={
-                          this.state.isEditMode
-                            ? database =>
-                                this.onDatasourcePropChange(
-                                  'database',
-                                  database,
-                                )
-                            : undefined
-                        }
-                        onTableChange={
-                          this.state.isEditMode
-                            ? table =>
-                                this.onDatasourcePropChange('table_name', table)
-                            : undefined
-                        }
-                        readOnly={!this.state.isEditMode}
-                      />
-                    </div>
+                    <TableSelector
+                      clearable={false}
+                      dbId={datasource.database.id}
+                      handleError={this.props.addDangerToast}
+                      schema={datasource.schema}
+                      sqlLabMode={false}
+                      tableName={datasource.table_name}
+                      onSchemaChange={
+                        this.state.isEditMode
+                          ? schema =>
+                              this.onDatasourcePropChange('schema', schema)
+                          : undefined
+                      }
+                      onDbChange={
+                        this.state.isEditMode
+                          ? database =>
+                              this.onDatasourcePropChange('database', database)
+                          : undefined
+                      }
+                      onTableChange={
+                        this.state.isEditMode
+                          ? table =>
+                              this.onDatasourcePropChange('table_name', table)
+                          : undefined
+                      }
+                      readOnly={!this.state.isEditMode}
+                    />
                   }
                   description={t(
                     'The pointer to a physical table (or view). Keep in mind that the chart is ' +
diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
index 3df5532..9278c29 100644
--- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
+++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
@@ -227,7 +227,10 @@ class DatasourceControl extends React.PureComponent {
             </Tooltip>
           )}
           {extra?.warning_markdown && (
-            <WarningIconWithTooltip warningMarkdown={extra.warning_markdown} />
+            <WarningIconWithTooltip
+              warningMarkdown={extra.warning_markdown}
+              size={30}
+            />
           )}
           <Dropdown
             overlay={datasourceMenu}
diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
index f50473c..496decf 100644
--- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
+++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
@@ -243,13 +243,11 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
                   <CertifiedIcon
                     certifiedBy={parsedExtra.certification.certified_by}
                     details={parsedExtra.certification.details}
-                    size="l"
                   />
                 )}
                 {parsedExtra?.warning_markdown && (
                   <WarningIconWithTooltip
                     warningMarkdown={parsedExtra.warning_markdown}
-                    size="l"
                   />
                 )}
                 {titleLink}
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index 261a7d7..8d8eb6e 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -160,7 +160,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
         "url",
         "extra",
     ]
-    show_columns = show_select_columns + ["columns.type_generic", "database.backend"]
+    show_columns = show_select_columns + ["columns.type_generic"]
     add_model_schema = DatasetPostSchema()
     edit_model_schema = DatasetPutSchema()
     add_columns = ["database", "schema", "table_name", "owners"]
diff --git a/superset/views/core.py b/superset/views/core.py
index 29109f1..d7e626e 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1058,14 +1058,8 @@ class Superset(BaseSupersetView):  # pylint: disable=too-many-public-methods
     @event_logger.log_this
     @expose("/tables/<int:db_id>/<schema>/<substr>/")
     @expose("/tables/<int:db_id>/<schema>/<substr>/<force_refresh>/")
-    @expose("/tables/<int:db_id>/<schema>/<substr>/<force_refresh>/<exact_match>")
-    def tables(  # pylint: disable=too-many-locals,no-self-use,too-many-arguments
-        self,
-        db_id: int,
-        schema: str,
-        substr: str,
-        force_refresh: str = "false",
-        exact_match: str = "false",
+    def tables(  # pylint: disable=too-many-locals,no-self-use
+        self, db_id: int, schema: str, substr: str, force_refresh: str = "false"
     ) -> FlaskResponse:
         """Endpoint to fetch the list of tables for given database"""
         # Guarantees database filtering by security access
@@ -1078,7 +1072,6 @@ class Superset(BaseSupersetView):  # pylint: disable=too-many-public-methods
             return json_error_response("Not found", 404)
 
         force_refresh_parsed = force_refresh.lower() == "true"
-        exact_match_parsed = exact_match.lower() == "true"
         schema_parsed = utils.parse_js_uri_path_item(schema, eval_undefined=True)
         substr_parsed = utils.parse_js_uri_path_item(substr, eval_undefined=True)
 
@@ -1120,15 +1113,9 @@ class Superset(BaseSupersetView):  # pylint: disable=too-many-public-methods
                 ds_name.table if schema_parsed else f"{ds_name.schema}.{ds_name.table}"
             )
 
-        def is_match(src: str, target: utils.DatasourceName) -> bool:
-            target_label = get_datasource_label(target)
-            if exact_match_parsed:
-                return src == target_label
-            return src in target_label
-
         if substr_parsed:
-            tables = [tn for tn in tables if is_match(substr_parsed, tn)]
-            views = [vn for vn in views if is_match(substr_parsed, vn)]
+            tables = [tn for tn in tables if substr_parsed in get_datasource_label(tn)]
+            views = [vn for vn in views if substr_parsed in get_datasource_label(vn)]
 
         if not schema_parsed and database.default_schemas:
             user_schemas = (
diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py
index c970153..385025e 100644
--- a/tests/integration_tests/datasets/api_tests.py
+++ b/tests/integration_tests/datasets/api_tests.py
@@ -222,7 +222,6 @@ class TestDatasetApi(SupersetTestCase):
         Dataset API: Test get dataset item
         """
         table = self.get_energy_usage_dataset()
-        main_db = get_main_database()
         self.login(username="admin")
         uri = f"api/v1/dataset/{table.id}"
         rv = self.get_assert_metric(uri, "get")
@@ -230,11 +229,7 @@ class TestDatasetApi(SupersetTestCase):
         response = json.loads(rv.data.decode("utf-8"))
         expected_result = {
             "cache_timeout": None,
-            "database": {
-                "backend": main_db.backend,
-                "database_name": "examples",
-                "id": 1,
-            },
+            "database": {"database_name": "examples", "id": 1},
             "default_endpoint": None,
             "description": "Energy consumption",
             "extra": None,
@@ -249,10 +244,9 @@ class TestDatasetApi(SupersetTestCase):
             "table_name": "energy_usage",
             "template_params": None,
         }
-        if response["result"]["database"]["backend"] not in ("presto", "hive"):
-            assert {
-                k: v for k, v in response["result"].items() if k in expected_result
-            } == expected_result
+        assert {
+            k: v for k, v in response["result"].items() if k in expected_result
+        } == expected_result
         assert len(response["result"]["columns"]) == 3
         assert len(response["result"]["metrics"]) == 2