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 <erik.rit...@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
 

Reply via email to